Merge "Add IPv6 support to create_websocket()"
diff --git a/.gitignore b/.gitignore
index 287db4c..7cb052f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,6 +18,7 @@
dist
build
.testrepository
+.stestr
.idea
.project
.pydevproject
diff --git a/.stestr.conf b/.stestr.conf
new file mode 100644
index 0000000..e3201c1
--- /dev/null
+++ b/.stestr.conf
@@ -0,0 +1,4 @@
+[DEFAULT]
+test_path=./tempest/test_discover
+group_regex=([^\.]*\.)*
+
diff --git a/.zuul.yaml b/.zuul.yaml
new file mode 100644
index 0000000..9f91455
--- /dev/null
+++ b/.zuul.yaml
@@ -0,0 +1,125 @@
+- job:
+ name: devstack-tempest
+ parent: devstack
+ description: Base Tempest job.
+ required-projects:
+ - openstack/tempest
+ timeout: 7200
+ roles:
+ - zuul: openstack-dev/devstack
+ vars:
+ devstack_services:
+ tempest: True
+ run: playbooks/devstack-tempest.yaml
+ post-run: playbooks/post-tempest.yaml
+
+- job:
+ name: tempest-full
+ parent: devstack-tempest
+ description: |
+ Base integration test with Neutron networking and py27.
+ Former names for this job where:
+ * legacy-tempest-dsvm-neutron-full
+ * gate-tempest-dsvm-neutron-full-ubuntu-xenial
+ vars:
+ tox_venvlist: full
+ devstack_localrc:
+ ENABLE_FILE_INJECTION: True
+
+- job:
+ name: tempest-tox-plugin-sanity-check
+ parent: tox
+ description: |
+ Run tempest plugin sanity check script using tox.
+ nodeset: ubuntu-xenial
+ vars:
+ tox_envlist: plugin-sanity-check
+ voting: false
+ timeout: 5000
+ required-projects:
+ - openstack/almanach
+ - openstack/aodh
+ - openstack/barbican-tempest-plugin
+ - openstack/ceilometer
+ - openstack/cinder
+ - openstack/congress
+ - openstack/designate-tempest-plugin
+ - openstack/ec2-api
+ - openstack/freezer
+ - openstack/freezer-api
+ - openstack/freezer-tempest-plugin
+ - openstack/gce-api
+ - openstack/glare
+ - openstack/heat
+ - openstack/intel-nfv-ci-tests
+ - openstack/ironic
+ - openstack/ironic-inspector
+ - openstack/keystone-tempest-plugin
+ - openstack/kingbird
+ - openstack/kuryr-tempest-plugin
+ - openstack/magnum
+ - openstack/magnum-tempest-plugin
+ - openstack/manila
+ - openstack/manila-tempest-plugin
+ - openstack/mistral
+ - openstack/mogan
+ - openstack/monasca-api
+ - openstack/monasca-log-api
+ - openstack/murano
+ - openstack/networking-bgpvpn
+ - openstack/networking-cisco
+ - openstack/networking-fortinet
+ - openstack/networking-generic-switch
+ - openstack/networking-l2gw
+ - openstack/networking-midonet
+ - openstack/networking-plumgrid
+ - openstack/networking-sfc
+ - openstack/neutron
+ - openstack/neutron-dynamic-routing
+ - openstack/neutron-fwaas
+ - openstack/neutron-lbaas
+ - openstack/neutron-tempest-plugin
+ - openstack/neutron-vpnaas
+ - openstack/nova-lxd
+ - openstack/novajoin-tempest-plugin
+ - openstack/octavia
+ - openstack/oswin-tempest-plugin
+ - openstack/panko
+ - openstack/patrole
+ - openstack/qinling
+ - openstack/requirements
+ - openstack/sahara-tests
+ - openstack/senlin
+ - openstack/senlin-tempest-plugin
+ - openstack/tap-as-a-service
+ - openstack/tempest-horizon
+ - openstack/trio2o
+ - openstack/trove
+ - openstack/valet
+ - openstack/vitrage
+ - openstack/vmware-nsx-tempest-plugin
+ - openstack/watcher-tempest-plugin
+ - openstack/zaqar-tempest-plugin
+ - openstack/zun-tempest-plugin
+
+- project:
+ name: openstack/tempest
+ check:
+ jobs:
+ - devstack-tempest:
+ files:
+ - ^playbooks/
+ - ^roles/
+ - ^.zuul.yaml$
+ - tempest-full:
+ voting: false
+ irrelevant-files:
+ - ^(test-|)requirements.txt$
+ - ^.*\.rst$
+ - ^doc/.*$
+ - ^etc/.*$
+ - ^releasenotes/.*$
+ - ^setup.cfg$
+ - ^tempest/hacking/.*$
+ - ^tempest/tests/.*$
+ - tempest-tox-plugin-sanity-check
diff --git a/HACKING.rst b/HACKING.rst
index cc1f161..a3e9c26 100644
--- a/HACKING.rst
+++ b/HACKING.rst
@@ -9,14 +9,14 @@
------------------------------
- [T102] Cannot import OpenStack python clients in tempest/api &
- tempest/scenario tests
+ 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
- decorators.skip_because from tempest.lib
+ decorators.skip_because from tempest.lib
- [T110] Check that service client names of GET should be consistent
- [T111] Check that service client names of DELETE should be consistent
- [T112] Check that tempest.lib should not import local tempest code
@@ -84,7 +84,7 @@
It is recommended to use testtools `matcher`_ for the more tricky assertions.
You can implement your own specific `matcher`_ as well.
-.. _matcher: http://testtools.readthedocs.org/en/latest/for-test-authors.html#matchers
+.. _matcher: https://testtools.readthedocs.org/en/latest/for-test-authors.html#matchers
If the test case fails you can see the related logs and the information
carried by the exception (exception class, backtrack and exception info).
@@ -103,10 +103,10 @@
Service Tagging
---------------
Service tagging is used to specify which services are exercised by a particular
-test method. You specify the services with the ``tempest.test.services``
+test method. You specify the services with the ``tempest.common.utils.services``
decorator. For example:
-@services('compute', 'image')
+@utils.services('compute', 'image')
Valid service tag names are the same as the list of directories in tempest.api
that have tests.
@@ -121,32 +121,38 @@
Test fixtures and resources
---------------------------
Test level resources should be cleaned-up after the test execution. Clean-up
-is best scheduled using `addCleanup` which ensures that the resource cleanup
+is best scheduled using ``addCleanup`` which ensures that the resource cleanup
code is always invoked, and in reverse order with respect to the creation
order.
-Test class level resources should be defined in the `resource_setup` method of
-the test class, except for any credential obtained from the credentials
-provider, which should be set-up in the `setup_credentials` method.
+Test class level resources should be defined in the ``resource_setup`` method
+of the test class, except for any credential obtained from the credentials
+provider, which should be set-up in the ``setup_credentials`` method.
+Cleanup is best scheduled using ``addClassResourceCleanup`` which ensures that
+the cleanup code is always invoked, and in reverse order with respect to the
+creation order.
-The test base class `BaseTestCase` defines Tempest framework for class level
-fixtures. `setUpClass` and `tearDownClass` are defined here and cannot be
+In both cases - test level and class level cleanups - a wait loop should be
+scheduled before the actual delete of resources with an asynchronous delete.
+
+The test base class ``BaseTestCase`` defines Tempest framework for class level
+fixtures. ``setUpClass`` and ``tearDownClass`` are defined here and cannot be
overwritten by subclasses (enforced via hacking rule T105).
Set-up is split in a series of steps (setup stages), which can be overwritten
by test classes. Set-up stages are:
-- `skip_checks`
-- `setup_credentials`
-- `setup_clients`
-- `resource_setup`
+- ``skip_checks``
+- ``setup_credentials``
+- ``setup_clients``
+- ``resource_setup``
Tear-down is also split in a series of steps (teardown stages), which are
stacked for execution only if the corresponding setup stage had been
reached during the setup phase. Tear-down stages are:
-- `clear_credentials` (defined in the base test class)
-- `resource_cleanup`
+- ``clear_credentials`` (defined in the base test class)
+- ``resource_cleanup``
Skipping Tests
--------------
@@ -172,7 +178,7 @@
All negative tests should be based on `API-WG guideline`_ . Such negative
tests can block any changes from accurate failure code to invalid one.
-.. _API-WG guideline: http://specs.openstack.org/openstack/api-wg/guidelines/http.html#failure-code-clarifications
+.. _API-WG guideline: https://specs.openstack.org/openstack/api-wg/guidelines/http.html#failure-code-clarifications
If facing some gray area which is not clarified on the above guideline, propose
a new guideline to the API-WG. With a proposal to the API-WG we will be able to
diff --git a/README.rst b/README.rst
index 17d4cba..242f4d5 100644
--- a/README.rst
+++ b/README.rst
@@ -2,7 +2,7 @@
Team and repository tags
========================
-.. image:: http://governance.openstack.org/badges/tempest.svg
+.. image:: https://governance.openstack.org/badges/tempest.svg
:target: https://governance.openstack.org/tc/reference/tags/index.html
.. Change things from this point on
@@ -61,7 +61,7 @@
#. You first need to install Tempest. This is done with pip after you check out
the Tempest repo::
- $ git clone http://git.openstack.org/openstack/tempest
+ $ git clone https://git.openstack.org/openstack/tempest
$ pip install tempest/
This can be done within a venv, but the assumption for this guide is that
@@ -133,7 +133,7 @@
Release Versioning
------------------
-`Tempest Release Notes <http://docs.openstack.org/releasenotes/tempest>`_
+`Tempest Release Notes <https://docs.openstack.org/releasenotes/tempest>`_
shows what changes have been released on each version.
Tempest's released versions are broken into 2 sets of information. Depending on
@@ -183,16 +183,35 @@
Tempest also has a set of unit tests which test the Tempest code itself. These
tests can be run by specifying the test discovery path::
- $ OS_TEST_PATH=./tempest/tests testr run --parallel
+ $ stestr --test-path ./tempest/tests run
-By setting OS_TEST_PATH to ./tempest/tests it specifies that test discover
-should only be run on the unit test directory. The default value of OS_TEST_PATH
-is OS_TEST_PATH=./tempest/test_discover which will only run test discover on the
+By setting ``--test-path`` option to ./tempest/tests it specifies that test discover
+should only be run on the unit test directory. The default value of ``test_path``
+is ``test_path=./tempest/test_discover`` which will only run test discover on the
Tempest suite.
Alternatively, there are the py27 and py35 tox jobs which will run the unit
tests with the corresponding version of python.
+One common activity is to just run a single test, you can do this with tox
+simply by specifying to just run py27 or py35 tests against a single test::
+
+ $ tox -e py27 -- -n tempest.tests.test_microversions.TestMicroversionsTestsClass.test_config_version_none_23
+
+Or all tests in the test_microversions.py file::
+
+ $ tox -e py27 -- -n tempest.tests.test_microversions
+
+You may also use regular expressions to run any matching tests::
+
+ $ tox -e py27 -- test_microversions
+
+Additionally, when running a single test, or test-file, the ``-n/--no-discover``
+argument is no longer required, however it may perform faster if included.
+
+For more information on these options and details about stestr, please see the
+`stestr documentation <http://stestr.readthedocs.io/en/latest/MANUAL.html>`_.
+
Python 2.6
----------
diff --git a/REVIEWING.rst b/REVIEWING.rst
index 7d28320..766d0c6 100644
--- a/REVIEWING.rst
+++ b/REVIEWING.rst
@@ -2,7 +2,7 @@
======================
To start read the `OpenStack Common Review Checklist
-<http://docs.openstack.org/infra/manual/developers.html#peer-review>`_
+<https://docs.openstack.org/infra/manual/developers.html#peer-review>`_
Ensuring code is executed
@@ -16,7 +16,7 @@
If a new test is added that depends on a new config option (like a feature
flag), the commit message must reference a change in DevStack or DevStack-Gate
that enables the execution of this newly introduced test. This reference could
-either be a `Cross-Repository Dependency <http://docs.openstack.org/infra/
+either be a `Cross-Repository Dependency <https://docs.openstack.org/infra/
manual/developers.html#cross-repository-dependencies>`_ or a simple link
to a Gerrit review.
@@ -121,8 +121,8 @@
When to approve
---------------
- * Every patch needs two +2s before being approved.
- * Its ok to hold off on an approval until a subject matter expert reviews it
- * If a patch has already been approved but requires a trivial rebase to merge,
- you do not have to wait for a second +2, since the patch has already had
- two +2s.
+* Every patch needs two +2s before being approved.
+* Its ok to hold off on an approval until a subject matter expert reviews it
+* If a patch has already been approved but requires a trivial rebase to merge,
+ you do not have to wait for a second +2, since the patch has already had
+ two +2s.
diff --git a/doc/source/_extra/.htaccess b/doc/source/_extra/.htaccess
new file mode 100644
index 0000000..7745594
--- /dev/null
+++ b/doc/source/_extra/.htaccess
@@ -0,0 +1 @@
+redirectmatch 301 ^/developer/tempest/(.*) /tempest/latest/$1
diff --git a/doc/source/conf.py b/doc/source/conf.py
index 067eb81..0a061b8 100644
--- a/doc/source/conf.py
+++ b/doc/source/conf.py
@@ -154,6 +154,9 @@
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
+# Add any paths that contain "extra" files, such as .htaccess or
+# robots.txt.
+html_extra_path = ['_extra']
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
diff --git a/doc/source/configuration.rst b/doc/source/configuration.rst
index 8f2865a..e5f70d2 100644
--- a/doc/source/configuration.rst
+++ b/doc/source/configuration.rst
@@ -17,10 +17,10 @@
Tempest allows for configuring a set of admin credentials in the ``auth``
section, via the following parameters:
- #. ``admin_username``
- #. ``admin_password``
- #. ``admin_project_name``
- #. ``admin_domain_name``
+#. ``admin_username``
+#. ``admin_password``
+#. ``admin_project_name``
+#. ``admin_domain_name``
Admin credentials are not mandatory to run Tempest, but when provided they
can be used to:
@@ -47,9 +47,9 @@
to provide it with information about how it communicates with keystone.
This involves configuring the following options in the ``identity`` section:
- - ``auth_version``
- - ``uri``
- - ``uri_v3``
+- ``auth_version``
+- ``uri``
+- ``uri_v3``
The ``auth_version`` option is used to tell Tempest whether it should be using
Keystone's v2 or v3 api for communicating with Keystone. The two uri options are
@@ -74,12 +74,12 @@
an admin user, and an alternate user. To enable and use dynamic credentials you
only need to configure two things:
- #. A set of admin credentials with permissions to create users and
- projects. This is specified in the ``auth`` section with the
- ``admin_username``, ``admin_project_name``, ``admin_domain_name`` and
- ``admin_password`` options
- #. To enable dynamic credentials in the ``auth`` section with the
- ``use_dynamic_credentials`` option.
+#. A set of admin credentials with permissions to create users and
+ projects. This is specified in the ``auth`` section with the
+ ``admin_username``, ``admin_project_name``, ``admin_domain_name`` and
+ ``admin_password`` options
+#. To enable dynamic credentials in the ``auth`` section with the
+ ``use_dynamic_credentials`` option.
This is also currently the default credential provider enabled by Tempest, due
to its common use and ease of configuration.
@@ -115,21 +115,21 @@
To enable and use locking test accounts you need do a few things:
- #. Create an accounts.yaml file which contains the set of pre-existing
- credentials to use for testing. To make sure you don't have a credentials
- starvation issue when running in parallel make sure you have at least two
- times the number of worker processes you are using to execute Tempest
- available in the file. (If running serially the worker count is 1.)
+#. Create an accounts.yaml file which contains the set of pre-existing
+ credentials to use for testing. To make sure you don't have a credentials
+ starvation issue when running in parallel make sure you have at least two
+ times the number of worker processes you are using to execute Tempest
+ available in the file. (If running serially the worker count is 1.)
- You can check the accounts.yaml.sample file packaged in Tempest for the yaml
- format.
- #. Provide Tempest with the location of your accounts.yaml file with the
- ``test_accounts_file`` option in the ``auth`` section
+ You can check the accounts.yaml.sample file packaged in Tempest for the yaml
+ format.
+#. Provide Tempest with the location of your accounts.yaml file with the
+ ``test_accounts_file`` option in the ``auth`` section
- *NOTE: Be sure to use a full path for the file; otherwise Tempest will
- likely not find it.*
+ *NOTE: Be sure to use a full path for the file; otherwise Tempest will
+ likely not find it.*
- #. Set ``use_dynamic_credentials = False`` in the ``auth`` group
+#. Set ``use_dynamic_credentials = False`` in the ``auth`` group
It is worth pointing out that each set of credentials in the accounts.yaml
should have a unique project. This is required to provide proper isolation
@@ -162,8 +162,8 @@
can use to boot the servers with. There are two options in the Tempest config
for doing this:
- #. ``flavor_ref``
- #. ``flavor_ref_alt``
+#. ``flavor_ref``
+#. ``flavor_ref_alt``
Both of these options are in the ``compute`` section of the config file and take
in the flavor id (not the name) from Nova. The ``flavor_ref`` option is what
@@ -181,8 +181,8 @@
Just like with flavors, Tempest needs to know which images to use for booting
servers. There are two options in the compute section just like with flavors:
- #. ``image_ref``
- #. ``image_ref_alt``
+#. ``image_ref``
+#. ``image_ref_alt``
Both options are expecting an image id (not name) from Nova. The ``image_ref``
option is what will be used for booting the majority of servers in Tempest.
@@ -192,13 +192,13 @@
There are also options in the ``scenario`` section for images:
- #. ``img_file``
- #. ``img_dir``
- #. ``aki_img_file``
- #. ``ari_img_file``
- #. ``ami_img_file``
- #. ``img_container_format``
- #. ``img_disk_format``
+#. ``img_file``
+#. ``img_dir``
+#. ``aki_img_file``
+#. ``ari_img_file``
+#. ``ami_img_file``
+#. ``img_container_format``
+#. ``img_disk_format``
However, unlike the other image options, these are used for a very small subset
of scenario tests which are uploading an image. These options are used to tell
@@ -261,7 +261,7 @@
To set a fixed network name simply:
- #. Set the ``fixed_network_name`` option in the ``compute`` group
+#. Set the ``fixed_network_name`` option in the ``compute`` group
In the case that the configured fixed network name can not be found by a user
network list call, it will be treated like one was not provided except that a
@@ -329,9 +329,9 @@
To enable remote access to servers, there are 3 options at a minimum that are used:
- #. ``run_validation``
- #. ``connect_method``
- #. ``auth_method``
+#. ``run_validation``
+#. ``connect_method``
+#. ``auth_method``
The ``run_validation`` is used to enable or disable ssh connectivity for
all tests (with the exception of scenario tests which do not have a flag for
@@ -370,9 +370,9 @@
service catalog. There are three options for each service section to accomplish
this:
- #. ``catalog_type``
- #. ``endpoint_type``
- #. ``region``
+#. ``catalog_type``
+#. ``endpoint_type``
+#. ``region``
Setting ``catalog_type`` and ``endpoint_type`` should normally give Tempest
enough information to determine which endpoint it should pull from the service
diff --git a/data/tempest-plugins-registry.header b/doc/source/data/tempest-plugins-registry.header
similarity index 100%
rename from data/tempest-plugins-registry.header
rename to doc/source/data/tempest-plugins-registry.header
diff --git a/doc/source/library.rst b/doc/source/library.rst
index a461a0f..14415ae 100644
--- a/doc/source/library.rst
+++ b/doc/source/library.rst
@@ -44,9 +44,9 @@
existing interfaces we have to be careful to make sure we don't break any
external consumers. Some common red flags are:
- * a change to an existing API requires a change outside the library directory
- where the interface is being consumed
- * a unit test has to be significantly changed to make the proposed change pass
+* a change to an existing API requires a change outside the library directory
+ where the interface is being consumed
+* a unit test has to be significantly changed to make the proposed change pass
Testing
'''''''
@@ -69,3 +69,4 @@
library/auth
library/clients
library/credential_providers
+ library/validation_resources
diff --git a/doc/source/library/validation_resources.rst b/doc/source/library/validation_resources.rst
new file mode 100644
index 0000000..9b36476
--- /dev/null
+++ b/doc/source/library/validation_resources.rst
@@ -0,0 +1,11 @@
+.. _validation_resources:
+
+Validation Resources
+====================
+
+-------------------------------
+The validation_resources module
+-------------------------------
+
+.. automodule:: tempest.lib.common.validation_resources
+ :members:
diff --git a/doc/source/microversion_testing.rst b/doc/source/microversion_testing.rst
index 307eb07..7189312 100644
--- a/doc/source/microversion_testing.rst
+++ b/doc/source/microversion_testing.rst
@@ -33,46 +33,46 @@
Tempest will cover only integration testing of applicable microversions with
below exceptions:
- #. Test covers a feature which is important for interoperability. This covers tests requirement
- from Defcore.
- #. Test needed to fill Schema gaps.
- Tempest validates API responses with defined JSON schema. API responses can be different on
- each microversion and the JSON schemas need to be defined separately for the microversion.
- While implementing new integration tests for a specific microversion, there
- may be a gap in the JSON schemas (caused by previous microversions) implemented
- in Tempest.
- Filling that gap while implementing the new integration test cases is not efficient due to
- many reasons:
+#. Test covers a feature which is important for interoperability. This covers tests requirement
+ from Defcore.
+#. Test needed to fill Schema gaps.
+ Tempest validates API responses with defined JSON schema. API responses can be different on
+ each microversion and the JSON schemas need to be defined separately for the microversion.
+ While implementing new integration tests for a specific microversion, there
+ may be a gap in the JSON schemas (caused by previous microversions) implemented
+ in Tempest.
+ Filling that gap while implementing the new integration test cases is not efficient due to
+ many reasons:
- * Hard to review
- * Sync between multiple integration tests patches which try to fill the same schema gap at same
- time
- * Might delay the microversion change on project side where project team wants Tempest
- tests to verify the results.
+ * Hard to review
+ * Sync between multiple integration tests patches which try to fill the same schema gap at same
+ time
+ * Might delay the microversion change on project side where project team wants Tempest
+ tests to verify the results.
- Tempest will allow to fill the schema gaps at the end of each cycle, or more
- often if required.
- Schema gap can be filled with testing those with a minimal set of tests. Those
- tests might not be integration tests and might be already covered on project
- side also.
- This exception is needed because:
+ Tempest will allow to fill the schema gaps at the end of each cycle, or more
+ often if required.
+ Schema gap can be filled with testing those with a minimal set of tests. Those
+ tests might not be integration tests and might be already covered on project
+ side also.
+ This exception is needed because:
- * Allow to create microversion response schema in Tempest at the same time that projects are
- implementing their API microversions. This will make implementation easier for adding
- required tests before a new microversion change can be merged in the corresponding project
- and hence accelerate the development of microversions.
- * New schema must be verified by at least one test case which exercises such schema.
+ * Allow to create microversion response schema in Tempest at the same time that projects are
+ implementing their API microversions. This will make implementation easier for adding
+ required tests before a new microversion change can be merged in the corresponding project
+ and hence accelerate the development of microversions.
+ * New schema must be verified by at least one test case which exercises such schema.
- For example:
- If any projects implemented 4 API microversion say- v2.3, v2.4, v2.5, v2.6
- Assume microversion v2.3, v2.4, v2.6 change the API Response which means Tempest
- needs to add JSON schema for v2.3, v2.4, v2.6.
- In that case if only 1 or 2 tests can verify all new schemas then we do not need
- separate tests for each new schemas. In worst case, we have to add 3 separate tests.
- #. Test covers service behavior at large scale with involvement of more deep layer like hypervisor
- etc not just API/DB layer. This type of tests will be added case by case basis and
- with project team consultation about why it cannot be covered on project side and worth to test
- in Tempest.
+ For example:
+ If any projects implemented 4 API microversion say- v2.3, v2.4, v2.5, v2.6
+ Assume microversion v2.3, v2.4, v2.6 change the API Response which means Tempest
+ needs to add JSON schema for v2.3, v2.4, v2.6.
+ In that case if only 1 or 2 tests can verify all new schemas then we do not need
+ separate tests for each new schemas. In worst case, we have to add 3 separate tests.
+#. Test covers service behavior at large scale with involvement of more deep layer like hypervisor
+ etc not just API/DB layer. This type of tests will be added case by case basis and
+ with project team consultation about why it cannot be covered on project side and worth to test
+ in Tempest.
Project Scope For Microversion Testing
""""""""""""""""""""""""""""""""""""""
@@ -294,48 +294,72 @@
* Compute
- * `2.1`_
+ * `2.1`_
- .. _2.1: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id1
+ .. _2.1: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id1
- * `2.2`_
+ * `2.2`_
- .. _2.2: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id2
+ .. _2.2: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id2
- * `2.10`_
+ * `2.10`_
- .. _2.10: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id9
+ .. _2.10: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id9
- * `2.20`_
+ * `2.20`_
- .. _2.20: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id18
+ .. _2.20: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id18
- * `2.25`_
+ * `2.25`_
- .. _2.25: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#maximum-in-mitaka
+ .. _2.25: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#maximum-in-mitaka
- * `2.32`_
+ * `2.32`_
- .. _2.32: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id29
+ .. _2.32: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id29
- * `2.37`_
+ * `2.37`_
- .. _2.37: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id34
+ .. _2.37: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id34
- * `2.42`_
+ * `2.42`_
- .. _2.42: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#maximum-in-ocata
+ .. _2.42: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#maximum-in-ocata
- * `2.47`_
+ * `2.47`_
- .. _2.47: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id42
+ .. _2.47: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id42
- * `2.48`_
+ * `2.48`_
- .. _2.48: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id43
+ .. _2.48: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id43
* Volume
- * `3.3`_
+ * `3.3`_
- .. _3.3: https://docs.openstack.org/cinder/latest/contributor/api_microversion_history.html#id4
+ .. _3.3: https://docs.openstack.org/cinder/latest/contributor/api_microversion_history.html#id3
+
+ * `3.9`_
+
+ .. _3.9: https://docs.openstack.org/cinder/latest/contributor/api_microversion_history.html#id9
+
+ * `3.11`_
+
+ .. _3.11: https://docs.openstack.org/cinder/latest/contributor/api_microversion_history.html#id11
+
+ * `3.12`_
+
+ .. _3.12: https://docs.openstack.org/cinder/latest/contributor/api_microversion_history.html#id12
+
+ * `3.14`_
+
+ .. _3.14: https://docs.openstack.org/cinder/latest/contributor/api_microversion_history.html#id14
+
+ * `3.19`_
+
+ .. _3.19: https://docs.openstack.org/cinder/latest/contributor/api_microversion_history.html#id18
+
+ * `3.20`_
+
+ .. _3.20: https://docs.openstack.org/cinder/latest/contributor/api_microversion_history.html#id19
diff --git a/doc/source/plugin.rst b/doc/source/plugin.rst
index 77ef9ed..6f6621d 100644
--- a/doc/source/plugin.rst
+++ b/doc/source/plugin.rst
@@ -29,6 +29,8 @@
* tempest.config
* tempest.test_discover.plugins
* tempest.common.credentials_factory
+* tempest.clients
+* tempest.test
If there is an interface from tempest that you need to rely on in your plugin
which is not listed above, it likely needs to be migrated to tempest.lib. In
@@ -130,7 +132,7 @@
Plugin Structure
================
-While there are no hard and fast rules for the structure a plugin, there are
+While there are no hard and fast rules for the structure of a plugin, there are
basically no constraints on what the plugin looks like as long as the 2 steps
above are done. However, there are some recommended patterns to follow to make
it easy for people to contribute and work with your plugin. For example, if you
diff --git a/doc/source/test_removal.rst b/doc/source/test_removal.rst
index 07c3046..ddae6e2 100644
--- a/doc/source/test_removal.rst
+++ b/doc/source/test_removal.rst
@@ -29,19 +29,19 @@
In the proposal etherpad we'll be looking for answers to 3 questions
- #. The tests proposed for removal must have equiv. coverage in a different
- project's test suite (whether this is another gating test project, or an in
- tree functional test suite). For API tests preferably the other project will
- have a similar source of friction in place to prevent breaking api changes
- so that we don't regress and let breaking api changes slip through the
- gate.
- #. The test proposed for removal has a failure rate < 0.50% in the gate over
- the past release (the value and interval will likely be adjusted in the
- future)
+#. The tests proposed for removal must have equiv. coverage in a different
+ project's test suite (whether this is another gating test project, or an in
+ tree functional test suite). For API tests preferably the other project will
+ have a similar source of friction in place to prevent breaking api changes
+ so that we don't regress and let breaking api changes slip through the
+ gate.
+#. The test proposed for removal has a failure rate < 0.50% in the gate over
+ the past release (the value and interval will likely be adjusted in the
+ future)
- .. _`prong #3`:
- #. There must not be an external user/consumer of tempest
- that depends on the test proposed for removal
+ .. _`prong #3`:
+#. There must not be an external user/consumer of tempest
+ that depends on the test proposed for removal
The answers to 1 and 2 are easy to verify. For 1 just provide a link to the new
test location. If you are linking to the tempest removal patch please also put
@@ -62,29 +62,34 @@
The Old Way using subunit2sql directly
""""""""""""""""""""""""""""""""""""""
-SELECT * from tests where test_id like "%test_id%";
-(where $test_id is the full test_id, but truncated to the class because of
-setUpClass or tearDownClass failures)
+``SELECT * from tests where test_id like "%test_id%";``
+(where ``$test_id`` is the full test_id, but truncated to the class because of
+``setUpClass`` or ``tearDownClass`` failures)
You can access the infra mysql subunit2sql db w/ read-only permissions with:
- * hostname: logstash.openstack.org
- * username: query
- * password: query
- * db_name: subunit2sql
+* hostname: logstash.openstack.org
+* username: query
+* password: query
+* db_name: subunit2sql
For example if you were trying to remove the test with the id:
-tempest.api.compute.admin.test_flavors_negative.FlavorsAdminNegativeTestJSON.test_get_flavor_details_for_deleted_flavor
+``tempest.api.compute.admin.test_flavors_negative.FlavorsAdminNegativeTestJSON.test_get_flavor_details_for_deleted_flavor``
you would run the following:
- #. run: "mysql -u query -p -h logstash.openstack.org subunit2sql" to connect
- to the subunit2sql db
- #. run the query: MySQL [subunit2sql]> select * from tests where test_id like
- "tempest.api.compute.admin.test_flavors_negative.FlavorsAdminNegativeTestJSON%";
- which will return a table of all the tests in the class (but it will also
- catch failures in setUpClass and tearDownClass)
- #. paste the output table with numbers and the mysql command you ran to
- generate it into the etherpad.
+#. run the command: ``mysql -u query -p -h logstash.openstack.org subunit2sql``
+ to connect to the subunit2sql db
+#. run the query:
+
+ .. code-block:: console
+
+ MySQL [subunit2sql]> select * from tests where test_id like \
+ "tempest.api.compute.admin.test_flavors_negative.FlavorsAdminNegativeTestJSON%";
+
+ which will return a table of all the tests in the class (but it will also
+ catch failures in ``setUpClass`` and ``tearDownClass``)
+#. paste the output table with numbers and the mysql command you ran to
+ generate it into the etherpad.
Eventually a cli interface will be created to make that a bit more friendly.
Also a dashboard is in the works so we don't need to manually run the command.
@@ -131,23 +136,23 @@
For the most part all tempest test removals have to go through this procedure
there are a couple of exceptions though:
- #. The class of testing has been decided to be outside the scope of tempest.
- #. A revert for a patch which added a broken test, or testing which didn't
- actually run in the gate (basically any revert for something which
- shouldn't have been added)
- #. Tests that would become out of scope as a consequence of an API change,
- as described in `API Compatibility`_.
- Such tests cannot live in Tempest because of the branchless nature of
- Tempest. Such test must still honor `prong #3`_.
+#. The class of testing has been decided to be outside the scope of tempest.
+#. A revert for a patch which added a broken test, or testing which didn't
+ actually run in the gate (basically any revert for something which
+ shouldn't have been added)
+#. Tests that would become out of scope as a consequence of an API change,
+ as described in `API Compatibility`_.
+ Such tests cannot live in Tempest because of the branchless nature of
+ Tempest. Such test must still honor `prong #3`_.
For the first exception type the only types of testing in tree which have been
declared out of scope at this point are:
- * The CLI tests (which should be completely removed at this point)
- * Neutron Adv. Services testing (which should be completely removed at this
- point)
- * XML API Tests (which should be completely removed at this point)
- * EC2 API/boto tests (which should be completely removed at this point)
+* The CLI tests (which should be completely removed at this point)
+* Neutron Adv. Services testing (which should be completely removed at this
+ point)
+* XML API Tests (which should be completely removed at this point)
+* EC2 API/boto tests (which should be completely removed at this point)
For tests that fit into this category the only criteria for removal is that
there is equivalent testing elsewhere.
@@ -159,12 +164,12 @@
are defined as in scope for direct testing in tempest. As of today that list
is:
- * Keystone
- * Nova
- * Glance
- * Cinder
- * Neutron
- * Swift
+* Keystone
+* Nova
+* Glance
+* Cinder
+* Neutron
+* Swift
anything that lives in tempest which doesn't test one of these projects can be
removed assuming there is equivalent testing elsewhere. Preferably using the
diff --git a/doc/source/write_tests.rst b/doc/source/write_tests.rst
index aec55e9..49af95a 100644
--- a/doc/source/write_tests.rst
+++ b/doc/source/write_tests.rst
@@ -36,12 +36,12 @@
In standard unittest the lifecycle of a TestCase can be described in the
following phases:
- #. setUpClass
- #. setUp
- #. Test Execution
- #. tearDown
- #. doCleanups
- #. tearDownClass
+#. setUpClass
+#. setUp
+#. Test Execution
+#. tearDown
+#. doCleanups
+#. tearDownClass
setUpClass
----------
@@ -54,15 +54,21 @@
To accomplish this you do **not** define a setUpClass function, instead there
are a number of predefined phases to setUpClass that are used. The phases are:
- * skip_checks
- * setup_credentials
- * setup_clients
- * resource_setup
+* skip_checks
+* setup_credentials
+* setup_clients
+* resource_setup
-which is executed in that order. An example of a TestCase which defines all
+which is executed in that order. Cleanup of resources provisioned during
+the resource_setup must be scheduled right after provisioning using
+the addClassResourceCleanp helper. The resource cleanups stacked this way
+are executed in reverse order during tearDownClass, before the cleanup of
+test credentials takes place. An example of a TestCase which defines all
of these would be::
+ from tempest.common import waiters
from tempest import config
+ from tempest.lib.common.utils import test_utils
from tempest import test
CONF = config.CONF
@@ -111,6 +117,13 @@
"""
super(TestExampleCase, cls).resource_setup()
cls.shared_server = cls.servers_client.create_server(...)
+ cls.addClassResourceCleanup(waiters.wait_for_server_termination,
+ cls.servers_client,
+ cls.shared_server['id'])
+ cls.addClassResourceCleanup(
+ test_utils.call_and_ignore_notfound_exc(
+ cls.servers_client.delete_server,
+ cls.shared_server['id']))
.. _credentials:
diff --git a/playbooks/devstack-tempest.yaml b/playbooks/devstack-tempest.yaml
new file mode 100644
index 0000000..a684984
--- /dev/null
+++ b/playbooks/devstack-tempest.yaml
@@ -0,0 +1,14 @@
+# Changes that run through devstack-tempest are likely to have an impact on
+# the devstack part of the job, so we keep devstack in the main play to
+# avoid zuul retrying on legitimate failures.
+- hosts: all
+ roles:
+ - run-devstack
+
+# We run tests only on one node, regardless how many nodes are in the system
+- hosts: tempest
+ roles:
+ - setup-tempest-run-dir
+ - setup-tempest-data-dir
+ - acl-devstack-files
+ - run-tempest
diff --git a/playbooks/post-tempest.yaml b/playbooks/post-tempest.yaml
new file mode 100644
index 0000000..820e4f6
--- /dev/null
+++ b/playbooks/post-tempest.yaml
@@ -0,0 +1,24 @@
+- hosts: all
+ become: true
+ vars:
+ logs_root: "{{ devstack_base_dir|default('/opt/stack') }}"
+ stage_dir: "{{ devstack_base_dir|default('/opt/stack') }}"
+ test_results_stage_name: 'test_results'
+ roles:
+ - role: process-test-results
+ test_results_dir: '{{ logs_root }}/tempest'
+ tox_envdir: tempest
+ - role: process-stackviz
+ - role: stage-output
+ zuul_copy_output:
+ { '{{ logs_root }}/tempest/etc/tempest.conf': 'logs',
+ '{{ logs_root }}/tempest/etc/accounts.yaml': 'logs',
+ '{{ logs_root }}/tempest/tempest.log': 'logs',
+ '{{ stage_dir }}/{{ test_results_stage_name }}.subunit': 'logs',
+ '{{ stage_dir }}/{{ test_results_stage_name }}.html': 'logs',
+ '{{ stage_dir }}/stackviz': 'logs' }
+ extensions_to_txt:
+ - conf
+ - log
+ - yaml
+ - yml
diff --git a/releasenotes/notes/add-domain-param-in-cliclient-a270fcf35c8f09e6.yaml b/releasenotes/notes/add-domain-param-in-cliclient-a270fcf35c8f09e6.yaml
new file mode 100644
index 0000000..87a6af9
--- /dev/null
+++ b/releasenotes/notes/add-domain-param-in-cliclient-a270fcf35c8f09e6.yaml
@@ -0,0 +1,17 @@
+---
+fixes:
+ - |
+ Allow to specify new domain parameters:
+
+ * `user_domain_name`
+ * `user_domain_id`
+ * `project_domain_name`
+ * `project_domain_id`
+
+ for CLIClient class, whose values will be substituted to
+ ``--os-user-domain-name``, ``--os-user-domain-id``,
+ ``--os-project-domain-name`` and ``--os-project-domain-id`` respectively
+ during command execution.
+
+ This allows to prevent possible test failures with authentication in
+ Keystone v3. Bug: #1719687
diff --git a/releasenotes/notes/add-load-list-cmd-35a4a2e6ea0a36fd.yaml b/releasenotes/notes/add-load-list-cmd-35a4a2e6ea0a36fd.yaml
new file mode 100644
index 0000000..403bbad
--- /dev/null
+++ b/releasenotes/notes/add-load-list-cmd-35a4a2e6ea0a36fd.yaml
@@ -0,0 +1,7 @@
+---
+features:
+ - |
+ Adds a new cli option to tempest run, --load-list <list-file>
+ to specify target tests to run from a list-file. The list-file
+ supports the output format of the tempest run --list-tests
+ command.
diff --git a/releasenotes/notes/add-reset-group-snapshot-status-api-to-v3-group-snapshots-client-248d41827daf2a0c.yaml b/releasenotes/notes/add-reset-group-snapshot-status-api-to-v3-group-snapshots-client-248d41827daf2a0c.yaml
new file mode 100644
index 0000000..76b395d
--- /dev/null
+++ b/releasenotes/notes/add-reset-group-snapshot-status-api-to-v3-group-snapshots-client-248d41827daf2a0c.yaml
@@ -0,0 +1,6 @@
+---
+features:
+ - |
+ Add reset group snapshot status API to v3 group_snapshots_client library,
+ min_microversion of this API is 3.19. This feature enables the possibility
+ to reset group snapshot status.
diff --git a/releasenotes/notes/add-reset-group-status-api-to-v3-groups-client-9aa048617c66756a.yaml b/releasenotes/notes/add-reset-group-status-api-to-v3-groups-client-9aa048617c66756a.yaml
new file mode 100644
index 0000000..a39c23b
--- /dev/null
+++ b/releasenotes/notes/add-reset-group-status-api-to-v3-groups-client-9aa048617c66756a.yaml
@@ -0,0 +1,6 @@
+---
+features:
+ - |
+ Add reset group status API to v3 groups_client library, min_microversion
+ of this API is 3.20. This feature enables the possibility to reset group
+ status.
diff --git a/releasenotes/notes/add-support-args-kwargs-in-call-until-true-a91k592h5a64exf7.yaml b/releasenotes/notes/add-support-args-kwargs-in-call-until-true-a91k592h5a64exf7.yaml
new file mode 100644
index 0000000..e23abe3
--- /dev/null
+++ b/releasenotes/notes/add-support-args-kwargs-in-call-until-true-a91k592h5a64exf7.yaml
@@ -0,0 +1,5 @@
+---
+features:
+ - Add support of args and kwargs when calling func in call_until_true,
+ also to log the cost time when call_until_true returns True or False
+ for debuggin.
diff --git a/releasenotes/notes/add-validation-resources-to-lib-dc2600c4324ca4d7.yaml b/releasenotes/notes/add-validation-resources-to-lib-dc2600c4324ca4d7.yaml
new file mode 100644
index 0000000..7814f4e
--- /dev/null
+++ b/releasenotes/notes/add-validation-resources-to-lib-dc2600c4324ca4d7.yaml
@@ -0,0 +1,7 @@
+---
+features:
+ - |
+ Add the `validation_resources` module to tempest.lib. The module provides
+ a set of helpers that can be used to provision and cleanup all the
+ resources required to perform ping / ssh tests against a virtual machine:
+ a keypair, a security group with targeted rules and a floating IP.
diff --git a/releasenotes/notes/add_proxy_url_get_credentials-aef66b085450513f.yaml b/releasenotes/notes/add_proxy_url_get_credentials-aef66b085450513f.yaml
new file mode 100644
index 0000000..94ab462
--- /dev/null
+++ b/releasenotes/notes/add_proxy_url_get_credentials-aef66b085450513f.yaml
@@ -0,0 +1,6 @@
+---
+features:
+ - |
+ Add the proxy_url optional parameter to the get_credentials method in
+ tempest/lib/auth.py so that that helper can be used when going through
+ and HTTP proxy.
diff --git a/releasenotes/notes/disable-identity-v2-testing-4ef1565d1a5aedcf.yaml b/releasenotes/notes/disable-identity-v2-testing-4ef1565d1a5aedcf.yaml
new file mode 100644
index 0000000..e5d4ab7
--- /dev/null
+++ b/releasenotes/notes/disable-identity-v2-testing-4ef1565d1a5aedcf.yaml
@@ -0,0 +1,7 @@
+---
+upgrade:
+ - |
+ As of the Queens release, tempest no longer tests the identity v2.0 API
+ because the majority of the v2.0 API have been removed from the identity
+ project. Once the Queens release reaches end-of-life, we can remove the
+ v2.0 tempest tests and clean up v2.0 testing cruft.
diff --git a/releasenotes/notes/drop-DEFAULT_PARAMS-bfcc2e7b74ef880b.yaml b/releasenotes/notes/drop-DEFAULT_PARAMS-bfcc2e7b74ef880b.yaml
new file mode 100644
index 0000000..c9a49a7
--- /dev/null
+++ b/releasenotes/notes/drop-DEFAULT_PARAMS-bfcc2e7b74ef880b.yaml
@@ -0,0 +1,13 @@
+---
+upgrade:
+ - |
+ Replace any call in your code to credentials_factory.DEFAULT_PARAMS with
+ a call to config.service_client_config().
+fixes:
+ - |
+ The credentials_factory module used to load configuration at import time
+ which caused configuration being loaded at test discovery time.
+ This was fixed by removing the DEFAULT_PARAMS variable. This variable
+ was redundant (and outdated), the same dictionary (but up to date) can
+ be obtained via invoking config.service_client_config() with no service
+ parameter.
diff --git a/releasenotes/notes/fix-list-group-snapshots-api-969d9321002c566c.yaml b/releasenotes/notes/fix-list-group-snapshots-api-969d9321002c566c.yaml
new file mode 100644
index 0000000..775a383
--- /dev/null
+++ b/releasenotes/notes/fix-list-group-snapshots-api-969d9321002c566c.yaml
@@ -0,0 +1,6 @@
+---
+fixes:
+ - |
+ Fix list_group_snapshots API in v3 group_snapshots_client: Bug#1715786.
+ The url path for list group snapshots with details API is changed from
+ ``?detail=True`` to ``/detail``.
diff --git a/releasenotes/notes/http_proxy_config-cb39b55520e84db5.yaml b/releasenotes/notes/http_proxy_config-cb39b55520e84db5.yaml
new file mode 100644
index 0000000..56969de
--- /dev/null
+++ b/releasenotes/notes/http_proxy_config-cb39b55520e84db5.yaml
@@ -0,0 +1,9 @@
+---
+features:
+ - Adds a new config options, ``proxy_url``. This options is used to configure
+ running tempest through a proxy server.
+ - The RestClient class in tempest.lib.rest_client has a new kwarg parameters,
+ ``proxy_url``, that is used to set a proxy server.
+ - A new class was added to tempest.lib.http, ClosingProxyHttp. This behaves
+ identically to ClosingHttp except that it requires a proxy url and will
+ establish a connection through a proxy
diff --git a/releasenotes/notes/intermediate-queens-release-2f9f305775fca454.yaml b/releasenotes/notes/intermediate-queens-release-2f9f305775fca454.yaml
new file mode 100644
index 0000000..1493b0b
--- /dev/null
+++ b/releasenotes/notes/intermediate-queens-release-2f9f305775fca454.yaml
@@ -0,0 +1,4 @@
+---
+prelude: >
+ This is an intermediate release during the Queens development cycle to
+ make new functionality available to plugins and other consumers.
diff --git a/releasenotes/notes/list-auth-domains-v3-endpoint-9ec60c7d3011c397.yaml b/releasenotes/notes/list-auth-domains-v3-endpoint-9ec60c7d3011c397.yaml
new file mode 100644
index 0000000..0f104cf
--- /dev/null
+++ b/releasenotes/notes/list-auth-domains-v3-endpoint-9ec60c7d3011c397.yaml
@@ -0,0 +1,6 @@
+---
+features:
+ - |
+ Add ``list_auth_domains`` API endpoint to the identity v3 client. This
+ allows the possibility of listing all domains a user has access to
+ via role assignments.
diff --git a/releasenotes/notes/make-object-storage-client-as-stable-interface-d1b07c7e8f17bef6.yaml b/releasenotes/notes/make-object-storage-client-as-stable-interface-d1b07c7e8f17bef6.yaml
new file mode 100644
index 0000000..2bba952
--- /dev/null
+++ b/releasenotes/notes/make-object-storage-client-as-stable-interface-d1b07c7e8f17bef6.yaml
@@ -0,0 +1,11 @@
+---
+features:
+ - |
+ Define below object storage service clients as libraries.
+ Add new service clients to the library interface so the
+ other projects can use these modules as stable libraries
+ without any maintenance changes.
+
+ * account_client
+ * container_client
+ * object_client
diff --git a/releasenotes/notes/remove-deprecated-apis-from-v2-volumes-client-3ca4a5db5fea518f.yaml b/releasenotes/notes/remove-deprecated-apis-from-v2-volumes-client-3ca4a5db5fea518f.yaml
new file mode 100644
index 0000000..c75da2e
--- /dev/null
+++ b/releasenotes/notes/remove-deprecated-apis-from-v2-volumes-client-3ca4a5db5fea518f.yaml
@@ -0,0 +1,11 @@
+---
+upgrade:
+ - |
+ Remove deprecated APIs from volume v2 volumes_client, and the deprecated
+ APIs are re-realized in volume v2 transfers_client.
+
+ * create_volume_transfer
+ * show_volume_transfer
+ * list_volume_transfers
+ * delete_volume_transfer
+ * accept_volume_transfer
diff --git a/releasenotes/notes/remove-deprecated-skip-decorators-f8b42d812d20b537.yaml b/releasenotes/notes/remove-deprecated-skip-decorators-f8b42d812d20b537.yaml
new file mode 100644
index 0000000..920bc5d
--- /dev/null
+++ b/releasenotes/notes/remove-deprecated-skip-decorators-f8b42d812d20b537.yaml
@@ -0,0 +1,5 @@
+---
+upgrade:
+ - |
+ Remove two deprecated skip decorators in ``config`` module:
+ ``skip_unless_config`` and ``skip_if_config``.
diff --git a/releasenotes/notes/remove-deprecated-skip_unless_attr-decorator-02bde59a00328f5c.yaml b/releasenotes/notes/remove-deprecated-skip_unless_attr-decorator-02bde59a00328f5c.yaml
new file mode 100644
index 0000000..621731d
--- /dev/null
+++ b/releasenotes/notes/remove-deprecated-skip_unless_attr-decorator-02bde59a00328f5c.yaml
@@ -0,0 +1,4 @@
+---
+upgrade:
+ - |
+ Remove the deprecated decorator ``skip_unless_attr`` in lib/decorators.py.
diff --git a/releasenotes/notes/remove-deprecated-volume-apis-from-v2-volumes-client-cf35e5b4cca89860.yaml b/releasenotes/notes/remove-deprecated-volume-apis-from-v2-volumes-client-cf35e5b4cca89860.yaml
new file mode 100644
index 0000000..12ac5b5
--- /dev/null
+++ b/releasenotes/notes/remove-deprecated-volume-apis-from-v2-volumes-client-cf35e5b4cca89860.yaml
@@ -0,0 +1,7 @@
+---
+upgrade:
+ - |
+ Remove deprecated APIs (``show_pools`` and ``show_backend_capabilities``)
+ from volume v2 volumes_client, and the deprecated APIs are re-realized in
+ volume v2 scheduler_stats_client (``list_pools``) and capabilities_client
+ (``show_backend_capabilities``) accordingly.
diff --git a/releasenotes/notes/remove-get-ipv6-addr-by-EUI64-c79972d799c7a430.yaml b/releasenotes/notes/remove-get-ipv6-addr-by-EUI64-c79972d799c7a430.yaml
new file mode 100644
index 0000000..609000c
--- /dev/null
+++ b/releasenotes/notes/remove-get-ipv6-addr-by-EUI64-c79972d799c7a430.yaml
@@ -0,0 +1,5 @@
+---
+upgrade:
+ - |
+ Remove deprecated get_ipv6_addr_by_EUI64 method from data_utils.
+ Use the same method from oslo_utils.netutils.
diff --git a/releasenotes/notes/test-clients-stable-for-plugin-90b1e7dc83f28ccd.yaml b/releasenotes/notes/test-clients-stable-for-plugin-90b1e7dc83f28ccd.yaml
new file mode 100644
index 0000000..e27ee33
--- /dev/null
+++ b/releasenotes/notes/test-clients-stable-for-plugin-90b1e7dc83f28ccd.yaml
@@ -0,0 +1,8 @@
+---
+features:
+ - |
+ Two extra modules are now marked as stable for plugins, test.py and clients.py.
+ The former includes the test base class with its automatic credentials
+ provisioning and test resource managing fixtures.
+ The latter is built on top of ServiceClients and it adds aliases and a few custom
+ configurations to it.
diff --git a/releasenotes/source/conf.py b/releasenotes/source/conf.py
index ae3dca1..57ec7e1 100644
--- a/releasenotes/source/conf.py
+++ b/releasenotes/source/conf.py
@@ -65,16 +65,12 @@
project = u'tempest Release Notes'
copyright = u'2016, tempest Developers'
-# The version info for the project you're documenting, acts as replacement for
-# |version| and |release|, also used in various other places throughout the
-# built documents.
-#
-# The short X.Y version.
-from tempest.version import version_info as tempest_version
+# Release do not need a version number in the title, they
+# cover multiple versions.
# The full version, including alpha/beta/rc tags.
-release = tempest_version.version_string_with_vcs()
+release = ''
# The short X.Y version.
-version = tempest_version.canonical_version_string()
+version = ''
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
diff --git a/requirements.txt b/requirements.txt
index 36b9efa..2300214 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -2,24 +2,24 @@
# of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later.
pbr!=2.1.0,>=2.0.0 # Apache-2.0
-cliff>=2.8.0 # Apache-2.0
-jsonschema!=2.5.0,<3.0.0,>=2.0.0 # MIT
-testtools>=1.4.0 # MIT
+cliff!=2.9.0,>=2.8.0 # Apache-2.0
+jsonschema<3.0.0,>=2.6.0 # MIT
+testtools>=2.2.0 # MIT
paramiko>=2.0.0 # LGPLv2.1+
-netaddr!=0.7.16,>=0.7.13 # BSD
+netaddr>=0.7.18 # BSD
testrepository>=0.0.18 # Apache-2.0/BSD
-oslo.concurrency>=3.8.0 # Apache-2.0
-oslo.config!=4.3.0,!=4.4.0,>=4.0.0 # Apache-2.0
+oslo.concurrency>=3.20.0 # Apache-2.0
+oslo.config>=5.1.0 # Apache-2.0
oslo.log>=3.30.0 # Apache-2.0
-oslo.serialization!=2.19.1,>=1.10.0 # Apache-2.0
-oslo.utils>=3.20.0 # Apache-2.0
-six>=1.9.0 # MIT
+oslo.serialization!=2.19.1,>=2.18.0 # Apache-2.0
+oslo.utils>=3.31.0 # Apache-2.0
+six>=1.10.0 # MIT
fixtures>=3.0.0 # Apache-2.0/BSD
PyYAML>=3.10 # MIT
-python-subunit>=0.0.18 # Apache-2.0/BSD
+python-subunit>=1.0.0 # Apache-2.0/BSD
stevedore>=1.20.0 # Apache-2.0
PrettyTable<0.8,>=0.7.1 # BSD
-os-testr>=0.8.0 # Apache-2.0
+os-testr>=1.0.0 # Apache-2.0
urllib3>=1.21.1 # MIT
debtcollector>=1.2.0 # Apache-2.0
-unittest2 # BSD
+unittest2>=1.1.0 # BSD
diff --git a/roles/acl-devstack-files/README.rst b/roles/acl-devstack-files/README.rst
new file mode 100644
index 0000000..76e7e58
--- /dev/null
+++ b/roles/acl-devstack-files/README.rst
@@ -0,0 +1,10 @@
+Grant global read access to devstack `files` folder.
+
+This is handy to grant the `tempest` user access to VM images for testing.
+
+**Role Variables**
+
+.. zuul:rolevar:: devstack_data_dir
+ :default: /opt/stack/data
+
+ The devstack data directory.
diff --git a/roles/acl-devstack-files/defaults/main.yaml b/roles/acl-devstack-files/defaults/main.yaml
new file mode 100644
index 0000000..14265f0
--- /dev/null
+++ b/roles/acl-devstack-files/defaults/main.yaml
@@ -0,0 +1 @@
+devstack_data_dir: /opt/stack/data
diff --git a/roles/acl-devstack-files/tasks/main.yaml b/roles/acl-devstack-files/tasks/main.yaml
new file mode 100644
index 0000000..b3eeec7
--- /dev/null
+++ b/roles/acl-devstack-files/tasks/main.yaml
@@ -0,0 +1,6 @@
+- name: Grant global read access to devstack files
+ file:
+ path: "{{devstack_data_dir}}/files"
+ mode: "o+rx"
+ recurse: yes
+ become: yes
diff --git a/roles/process-stackviz/README.rst b/roles/process-stackviz/README.rst
new file mode 100644
index 0000000..b05326d
--- /dev/null
+++ b/roles/process-stackviz/README.rst
@@ -0,0 +1,22 @@
+Generate stackviz report.
+
+Generate stackviz report using subunit and dstat data, using
+the stackviz archive embedded in test images.
+
+**Role Variables**
+
+.. zuul:rolevar:: devstack_base_dir
+ :default: /opt/stack
+
+ The devstack base directory.
+
+.. zuul:rolevar:: stage_dir
+ :default: /opt/stack/logs
+
+ The stage directory where the input data can be found and
+ the output will be produced.
+
+.. zuul:rolevar:: test_results_stage_name
+ :default: test_results
+
+ The name of the subunit file to be used as input.
diff --git a/roles/process-stackviz/defaults/main.yaml b/roles/process-stackviz/defaults/main.yaml
new file mode 100644
index 0000000..b1eb8d9
--- /dev/null
+++ b/roles/process-stackviz/defaults/main.yaml
@@ -0,0 +1,3 @@
+devstack_base_dir: /opt/stack
+stage_dir: /opt/stack/
+test_results_stage_name: test_results
diff --git a/roles/process-stackviz/tasks/main.yaml b/roles/process-stackviz/tasks/main.yaml
new file mode 100644
index 0000000..09de606
--- /dev/null
+++ b/roles/process-stackviz/tasks/main.yaml
@@ -0,0 +1,63 @@
+- name: Check if stackviz archive exists
+ stat:
+ path: "/opt/cache/files/stackviz-latest.tar.gz"
+ register: stackviz_archive
+
+- debug:
+ msg: "Stackviz archive could not be found in /opt/cache/files/stackviz-latest.tar.gz"
+ when: not stackviz_archive.stat.exists
+
+- name: Check if subunit data exists
+ stat:
+ path: "{{ stage_dir }}/{{ test_results_stage_name }}.subunit"
+ register: subunit_input
+
+- debug:
+ msg: "Subunit file could not be found at {{ stage_dir }}/{{ test_results_stage_name }}.subunit"
+ when: not subunit_input.stat.exists
+
+- name: Install stackviz
+ pip:
+ name: "file://{{ stackviz_archive.stat.path }}"
+ virtualenv: /tmp/stackviz
+ extra_args: -U
+ when:
+ - stackviz_archive.stat.exists
+ - subunit_input.stat.exists
+
+- name: Deploy stackviz static html+js
+ command: cp -pR /tmp/stackviz/share/stackviz-html {{ stage_dir }}/stackviz
+ when:
+ - stackviz_archive.stat.exists
+ - subunit_input.stat.exists
+
+- name: Check if dstat data exists
+ stat:
+ path: "{{ devstack_base_dir }}/logs/dstat-csv.log"
+ register: dstat_input
+ when:
+ - stackviz_archive.stat.exists
+ - subunit_input.stat.exists
+
+- name: Run stackviz with dstat
+ shell: |
+ cat {{ subunit_input.stat.path }} | \
+ /tmp/stackviz/bin/stackviz-export \
+ --dstat "{{ devstack_base_dir }}/logs/dstat-csv.log" \
+ --env --stdin \
+ {{ stage_dir }}/stackviz/data
+ when:
+ - stackviz_archive.stat.exists
+ - subunit_input.stat.exists
+ - dstat_input.stat.exists
+
+- name: Run stackviz without dstat
+ shell: |
+ cat {{ subunit_input.stat.path }} | \
+ /tmp/stackviz/bin/stackviz-export \
+ --env --stdin \
+ {{ stage_dir }}/stackviz/data
+ when:
+ - stackviz_archive.stat.exists
+ - subunit_input.stat.exists
+ - not dstat_input.stat.exists
diff --git a/roles/run-tempest/README.rst b/roles/run-tempest/README.rst
new file mode 100644
index 0000000..001586e
--- /dev/null
+++ b/roles/run-tempest/README.rst
@@ -0,0 +1,25 @@
+Run Tempest
+
+**Role Variables**
+
+.. zuul:rolevar:: devstack_base_dir
+ :default: /opt/stack
+
+ The devstack base directory.
+
+.. zuul:rolevar:: tempest_concurrency
+ :default: 0
+
+ The number of parallel test processes.
+
+.. zuul:rolevar:: tempest_test_regex
+ :default: ''
+
+ A regular expression used to select the tests.
+ It works only when used with some specific tox environments
+ ('all', 'all-plugin'.)
+
+.. zuul:rolevar:: tox_venvlist
+ :default: smoke
+
+ The Tempest tox environment to run.
diff --git a/roles/run-tempest/defaults/main.yaml b/roles/run-tempest/defaults/main.yaml
new file mode 100644
index 0000000..3e57511
--- /dev/null
+++ b/roles/run-tempest/defaults/main.yaml
@@ -0,0 +1,3 @@
+devstack_base_dir: /opt/stack
+tempest_test_regex: ''
+tox_venvlist: smoke
diff --git a/roles/run-tempest/tasks/main.yaml b/roles/run-tempest/tasks/main.yaml
new file mode 100644
index 0000000..297cd72
--- /dev/null
+++ b/roles/run-tempest/tasks/main.yaml
@@ -0,0 +1,28 @@
+# NOTE(andreaf) The number of vcpus is not available on all systems.
+# See https://github.com/ansible/ansible/issues/30688
+# When not available, we fall back to ansible_processor_cores
+- name: Get hw.logicalcpu from sysctl
+ shell: sysctl hw.logicalcpu | cut -d' ' -f2
+ register: sysctl_hw_logicalcpu
+ when: ansible_processor_vcpus is not defined
+
+- name: Number of cores
+ set_fact:
+ num_cores: "{{ansible_processor_vcpus|default(sysctl_hw_logicalcpu.stdout)}}"
+
+- name: Set concurrency for cores == 3 or less
+ set_fact:
+ default_concurrency: "{{ num_cores }}"
+ when: num_cores|int <= 3
+
+- name: Limit max concurrency when more than 3 vcpus are available
+ set_fact:
+ default_concurrency: "{{ num_cores|int // 2 }}"
+ when: num_cores|int > 3
+
+- name: Run Tempest
+ command: tox -e {{tox_venvlist}} -- {{tempest_test_regex|quote}} --concurrency={{tempest_concurrency|default(default_concurrency)}}
+ args:
+ chdir: "{{devstack_base_dir}}/tempest"
+ become: true
+ become_user: tempest
diff --git a/roles/setup-tempest-data-dir/README.rst b/roles/setup-tempest-data-dir/README.rst
new file mode 100644
index 0000000..db0b083
--- /dev/null
+++ b/roles/setup-tempest-data-dir/README.rst
@@ -0,0 +1,12 @@
+Setup the `tempest` user as owner of Tempest's data folder.
+
+Tempest's devstack plugin creates the data folder, but it has no knowledge
+of the `tempest` user, so we need a role to fix ownership on the data folder.
+
+
+**Role Variables**
+
+.. zuul:rolevar:: devstack_data_dir
+ :default: /opt/stack/data
+
+ The devstack data directory.
diff --git a/roles/setup-tempest-data-dir/defaults/main.yaml b/roles/setup-tempest-data-dir/defaults/main.yaml
new file mode 100644
index 0000000..14265f0
--- /dev/null
+++ b/roles/setup-tempest-data-dir/defaults/main.yaml
@@ -0,0 +1 @@
+devstack_data_dir: /opt/stack/data
diff --git a/roles/setup-tempest-data-dir/tasks/main.yaml b/roles/setup-tempest-data-dir/tasks/main.yaml
new file mode 100644
index 0000000..9dd6309
--- /dev/null
+++ b/roles/setup-tempest-data-dir/tasks/main.yaml
@@ -0,0 +1,7 @@
+- name: Set tempest as owner of Tempest data folder
+ file:
+ path: "{{devstack_data_dir}}/tempest"
+ owner: tempest
+ group: stack
+ recurse: yes
+ become: yes
diff --git a/roles/setup-tempest-run-dir/README.rst b/roles/setup-tempest-run-dir/README.rst
new file mode 100644
index 0000000..c8e2339
--- /dev/null
+++ b/roles/setup-tempest-run-dir/README.rst
@@ -0,0 +1,14 @@
+Setup Tempest run folder.
+
+To support isolation between multiple runs, separate run folders are required.
+Set `tempest` as owner of Tempest's current run folder.
+There is an implicit assumption here of a one to one relationship between
+devstack versions and Tempest runs.
+
+
+**Role Variables**
+
+.. zuul:rolevar:: devstack_base_dir
+ :default: /opt/stack
+
+ The devstack base directory.
diff --git a/roles/setup-tempest-run-dir/defaults/main.yaml b/roles/setup-tempest-run-dir/defaults/main.yaml
new file mode 100644
index 0000000..fea05c8
--- /dev/null
+++ b/roles/setup-tempest-run-dir/defaults/main.yaml
@@ -0,0 +1 @@
+devstack_base_dir: /opt/stack
diff --git a/roles/setup-tempest-run-dir/tasks/main.yaml b/roles/setup-tempest-run-dir/tasks/main.yaml
new file mode 100644
index 0000000..a012d72
--- /dev/null
+++ b/roles/setup-tempest-run-dir/tasks/main.yaml
@@ -0,0 +1,7 @@
+- name: Set tempest as owner of Tempest run folder
+ file:
+ path: "{{devstack_base_dir}}/tempest"
+ owner: tempest
+ group: stack
+ recurse: yes
+ become: yes
diff --git a/tempest/README.rst b/tempest/README.rst
index 663653e..62821de 100644
--- a/tempest/README.rst
+++ b/tempest/README.rst
@@ -12,10 +12,12 @@
and guidelines. Below is the overview of the Tempest respository structure
to make this clear.
-| tempest/
-| api/ - API tests
-| scenario/ - complex scenario tests
-| tests/ - unit tests for Tempest internals
+ .. code-block:: console
+
+ tempest/
+ api/ - API tests
+ scenario/ - complex scenario tests
+ tests/ - unit tests for Tempest internals
Each of these directories contains different types of tests. What
belongs in each directory, the rules and examples for good tests, are
diff --git a/tempest/api/compute/admin/test_aggregates_negative.py b/tempest/api/compute/admin/test_aggregates_negative.py
index 41be620..36ff09e 100644
--- a/tempest/api/compute/admin/test_aggregates_negative.py
+++ b/tempest/api/compute/admin/test_aggregates_negative.py
@@ -27,7 +27,6 @@
def setup_clients(cls):
super(AggregatesAdminNegativeTestJSON, cls).setup_clients()
cls.client = cls.os_admin.aggregates_client
- cls.user_client = cls.aggregates_client
@classmethod
def resource_setup(cls):
@@ -52,7 +51,7 @@
# Regular user is not allowed to create an aggregate.
aggregate_name = data_utils.rand_name(self.aggregate_name_prefix)
self.assertRaises(lib_exc.Forbidden,
- self.user_client.create_aggregate,
+ self.aggregates_client.create_aggregate,
name=aggregate_name)
@decorators.attr(type=['negative'])
@@ -87,7 +86,7 @@
# Regular user is not allowed to delete an aggregate.
aggregate = self._create_test_aggregate()
self.assertRaises(lib_exc.Forbidden,
- self.user_client.delete_aggregate,
+ self.aggregates_client.delete_aggregate,
aggregate['id'])
@decorators.attr(type=['negative'])
@@ -95,7 +94,7 @@
def test_aggregate_list_as_user(self):
# Regular user is not allowed to list aggregates.
self.assertRaises(lib_exc.Forbidden,
- self.user_client.list_aggregates)
+ self.aggregates_client.list_aggregates)
@decorators.attr(type=['negative'])
@decorators.idempotent_id('557cad12-34c9-4ff4-95f0-22f0dfbaf7dc')
@@ -103,7 +102,7 @@
# Regular user is not allowed to get aggregate details.
aggregate = self._create_test_aggregate()
self.assertRaises(lib_exc.Forbidden,
- self.user_client.show_aggregate,
+ self.aggregates_client.show_aggregate,
aggregate['id'])
@decorators.attr(type=['negative'])
@@ -140,7 +139,7 @@
# Regular user is not allowed to add a host to an aggregate.
aggregate = self._create_test_aggregate()
self.assertRaises(lib_exc.Forbidden,
- self.user_client.add_host,
+ self.aggregates_client.add_host,
aggregate['id'], host=self.host)
@decorators.attr(type=['negative'])
@@ -168,7 +167,7 @@
host=self.host)
self.assertRaises(lib_exc.Forbidden,
- self.user_client.remove_host,
+ self.aggregates_client.remove_host,
aggregate['id'], host=self.host)
@decorators.attr(type=['negative'])
diff --git a/tempest/api/compute/admin/test_auto_allocate_network.py b/tempest/api/compute/admin/test_auto_allocate_network.py
index 6f23866..a9772c4 100644
--- a/tempest/api/compute/admin/test_auto_allocate_network.py
+++ b/tempest/api/compute/admin/test_auto_allocate_network.py
@@ -143,6 +143,8 @@
test_utils.call_and_ignore_notfound_exc(
cls.networks_client.delete_network, network['id'])
+ super(AutoAllocateNetworkTest, cls).resource_cleanup()
+
@decorators.idempotent_id('5eb7b8fa-9c23-47a2-9d7d-02ed5809dd34')
def test_server_create_no_allocate(self):
"""Tests that no networking is allocated for the server."""
diff --git a/tempest/api/compute/admin/test_create_server.py b/tempest/api/compute/admin/test_create_server.py
index 66bedd9..08b2d19 100644
--- a/tempest/api/compute/admin/test_create_server.py
+++ b/tempest/api/compute/admin/test_create_server.py
@@ -17,8 +17,10 @@
from tempest.api.compute import base
from tempest.common.utils.linux import remote_client
+from tempest.common import waiters
from tempest import config
from tempest.lib.common.utils import data_utils
+from tempest.lib.common.utils import test_utils
from tempest.lib import decorators
CONF = config.CONF
@@ -35,12 +37,6 @@
super(ServersWithSpecificFlavorTestJSON, cls).setup_clients()
cls.client = cls.servers_client
- @classmethod
- def resource_setup(cls):
- cls.set_validation_resources()
-
- super(ServersWithSpecificFlavorTestJSON, cls).resource_setup()
-
@decorators.idempotent_id('b3c7bcfc-bb5b-4e22-b517-c7f686b802ca')
@testtools.skipUnless(CONF.validation.run_validation,
'Instance validation tests are disabled.')
@@ -67,20 +63,30 @@
admin_pass = self.image_ssh_password
+ validation_resources = self.get_test_validation_resources(
+ self.os_primary)
server_no_eph_disk = self.create_test_server(
validatable=True,
+ validation_resources=validation_resources,
wait_until='ACTIVE',
adminPass=admin_pass,
flavor=flavor_no_eph_disk_id)
+ self.addCleanup(waiters.wait_for_server_termination,
+ self.servers_client, server_no_eph_disk['id'])
+ self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+ self.servers_client.delete_server,
+ server_no_eph_disk['id'])
+
# Get partition number of server without ephemeral disk.
server_no_eph_disk = self.client.show_server(
server_no_eph_disk['id'])['server']
linux_client = remote_client.RemoteClient(
- self.get_server_ip(server_no_eph_disk),
+ self.get_server_ip(server_no_eph_disk,
+ validation_resources),
self.ssh_user,
admin_pass,
- self.validation_resources['keypair']['private_key'],
+ validation_resources['keypair']['private_key'],
server=server_no_eph_disk,
servers_client=self.client)
disks_num = len(linux_client.get_disks().split('\n'))
@@ -90,17 +96,25 @@
server_with_eph_disk = self.create_test_server(
validatable=True,
+ validation_resources=validation_resources,
wait_until='ACTIVE',
adminPass=admin_pass,
flavor=flavor_with_eph_disk_id)
+ self.addCleanup(waiters.wait_for_server_termination,
+ self.servers_client, server_with_eph_disk['id'])
+ self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+ self.servers_client.delete_server,
+ server_with_eph_disk['id'])
+
server_with_eph_disk = self.client.show_server(
server_with_eph_disk['id'])['server']
linux_client = remote_client.RemoteClient(
- self.get_server_ip(server_with_eph_disk),
+ self.get_server_ip(server_with_eph_disk,
+ validation_resources),
self.ssh_user,
admin_pass,
- self.validation_resources['keypair']['private_key'],
+ validation_resources['keypair']['private_key'],
server=server_with_eph_disk,
servers_client=self.client)
disks_num_eph = len(linux_client.get_disks().split('\n'))
diff --git a/tempest/api/compute/admin/test_fixed_ips.py b/tempest/api/compute/admin/test_fixed_ips.py
index ebba73c..66c2c2d 100644
--- a/tempest/api/compute/admin/test_fixed_ips.py
+++ b/tempest/api/compute/admin/test_fixed_ips.py
@@ -42,6 +42,7 @@
super(FixedIPsTestJson, cls).resource_setup()
server = cls.create_test_server(wait_until='ACTIVE')
server = cls.servers_client.show_server(server['id'])['server']
+ cls.ip = None
for ip_set in server['addresses']:
for ip in server['addresses'][ip_set]:
if ip['OS-EXT-IPS:type'] == 'fixed':
@@ -49,6 +50,9 @@
break
if cls.ip:
break
+ if cls.ip is None:
+ raise cls.skipException("No fixed ip found for server: %s"
+ % server['id'])
@decorators.idempotent_id('16b7d848-2f7c-4709-85a3-2dfb4576cc52')
def test_list_fixed_ip_details(self):
diff --git a/tempest/api/compute/admin/test_fixed_ips_negative.py b/tempest/api/compute/admin/test_fixed_ips_negative.py
index a5deb3c..7d41f46 100644
--- a/tempest/api/compute/admin/test_fixed_ips_negative.py
+++ b/tempest/api/compute/admin/test_fixed_ips_negative.py
@@ -43,6 +43,7 @@
super(FixedIPsNegativeTestJson, cls).resource_setup()
server = cls.create_test_server(wait_until='ACTIVE')
server = cls.servers_client.show_server(server['id'])['server']
+ cls.ip = None
for ip_set in server['addresses']:
for ip in server['addresses'][ip_set]:
if ip['OS-EXT-IPS:type'] == 'fixed':
@@ -50,6 +51,9 @@
break
if cls.ip:
break
+ if cls.ip is None:
+ raise cls.skipException("No fixed ip found for server: %s"
+ % server['id'])
@decorators.attr(type=['negative'])
@decorators.idempotent_id('9f17f47d-daad-4adc-986e-12370c93e407')
diff --git a/tempest/api/compute/admin/test_flavors_extra_specs.py b/tempest/api/compute/admin/test_flavors_extra_specs.py
index d4e7bd5..4d27a22 100644
--- a/tempest/api/compute/admin/test_flavors_extra_specs.py
+++ b/tempest/api/compute/admin/test_flavors_extra_specs.py
@@ -53,12 +53,11 @@
ephemeral=ephemeral,
swap=swap,
rxtx_factor=rxtx)['flavor']
-
- @classmethod
- def resource_cleanup(cls):
- cls.admin_flavors_client.delete_flavor(cls.flavor['id'])
- cls.admin_flavors_client.wait_for_resource_deletion(cls.flavor['id'])
- super(FlavorsExtraSpecsTestJSON, cls).resource_cleanup()
+ cls.addClassResourceCleanup(
+ cls.admin_flavors_client.wait_for_resource_deletion,
+ cls.flavor['id'])
+ cls.addClassResourceCleanup(cls.admin_flavors_client.delete_flavor,
+ cls.flavor['id'])
@decorators.idempotent_id('0b2f9d4b-1ca2-4b99-bb40-165d4bb94208')
def test_flavor_set_get_update_show_unset_keys(self):
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 bc32346..5cde39e 100644
--- a/tempest/api/compute/admin/test_flavors_extra_specs_negative.py
+++ b/tempest/api/compute/admin/test_flavors_extra_specs_negative.py
@@ -55,12 +55,11 @@
ephemeral=ephemeral,
swap=swap,
rxtx_factor=rxtx)['flavor']
-
- @classmethod
- def resource_cleanup(cls):
- cls.admin_flavors_client.delete_flavor(cls.flavor['id'])
- cls.admin_flavors_client.wait_for_resource_deletion(cls.flavor['id'])
- super(FlavorsExtraSpecsNegativeTestJSON, cls).resource_cleanup()
+ cls.addClassResourceCleanup(
+ cls.admin_flavors_client.wait_for_resource_deletion,
+ cls.flavor['id'])
+ cls.addClassResourceCleanup(cls.admin_flavors_client.delete_flavor,
+ cls.flavor['id'])
@decorators.attr(type=['negative'])
@decorators.idempotent_id('a00a3b81-5641-45a8-ab2b-4a8ec41e1d7d')
diff --git a/tempest/api/compute/admin/test_live_migration.py b/tempest/api/compute/admin/test_live_migration.py
index 14be947..411159b 100644
--- a/tempest/api/compute/admin/test_live_migration.py
+++ b/tempest/api/compute/admin/test_live_migration.py
@@ -46,6 +46,18 @@
"Less than 2 compute nodes, skipping migration test.")
@classmethod
+ def setup_credentials(cls):
+ # 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
+ # a subnet so the instance being migrated has a single port, but
+ # we need that to make sure we are properly updating the port
+ # host bindings during the live migration.
+ # TODO(mriedem): SSH validation before and after the instance is
+ # live migrated would be a nice test wrinkle addition.
+ cls.set_network_resources(network=True, subnet=True)
+ super(LiveMigrationTest, cls).setup_credentials()
+
+ @classmethod
def setup_clients(cls):
super(LiveMigrationTest, cls).setup_clients()
cls.admin_migration_client = cls.os_admin.migrations_client
diff --git a/tempest/api/compute/admin/test_quotas.py b/tempest/api/compute/admin/test_quotas.py
index 5894e80..c2bdf7e 100644
--- a/tempest/api/compute/admin/test_quotas.py
+++ b/tempest/api/compute/admin/test_quotas.py
@@ -17,6 +17,7 @@
from testtools import matchers
from tempest.api.compute import base
+from tempest.common import identity
from tempest.common import tempest_fixtures as fixtures
from tempest.lib.common.utils import data_utils
from tempest.lib import decorators
@@ -93,10 +94,11 @@
# Verify that GET shows the updated quota set of project
project_name = data_utils.rand_name('cpu_quota_project')
project_desc = project_name + '-desc'
- project = self.identity_utils.create_project(name=project_name,
- description=project_desc)
+ project = identity.identity_utils(self.os_admin).create_project(
+ name=project_name, description=project_desc)
project_id = project['id']
- self.addCleanup(self.identity_utils.delete_project, project_id)
+ self.addCleanup(identity.identity_utils(self.os_admin).delete_project,
+ project_id)
self.adm_client.update_quota_set(project_id, ram='5120')
quota_set = self.adm_client.show_quota_set(project_id)['quota_set']
@@ -106,12 +108,12 @@
user_name = data_utils.rand_name('cpu_quota_user')
password = data_utils.rand_password()
email = user_name + '@testmail.tm'
- user = self.identity_utils.create_user(username=user_name,
- password=password,
- project=project,
- email=email)
+ user = identity.identity_utils(self.os_admin).create_user(
+ username=user_name, password=password, project=project,
+ email=email)
user_id = user['id']
- self.addCleanup(self.identity_utils.delete_user, user_id)
+ self.addCleanup(identity.identity_utils(self.os_admin).delete_user,
+ user_id)
self.adm_client.update_quota_set(project_id,
user_id=user_id,
@@ -125,10 +127,11 @@
# Admin can delete the resource quota set for a project
project_name = data_utils.rand_name('ram_quota_project')
project_desc = project_name + '-desc'
- project = self.identity_utils.create_project(name=project_name,
- description=project_desc)
+ project = identity.identity_utils(self.os_admin).create_project(
+ name=project_name, description=project_desc)
project_id = project['id']
- self.addCleanup(self.identity_utils.delete_project, project_id)
+ self.addCleanup(identity.identity_utils(self.os_admin).delete_project,
+ project_id)
quota_set_default = (self.adm_client.show_quota_set(project_id)
['quota_set'])
ram_default = quota_set_default['ram']
diff --git a/tempest/api/compute/admin/test_servers_negative.py b/tempest/api/compute/admin/test_servers_negative.py
index 3656770..f720b84 100644
--- a/tempest/api/compute/admin/test_servers_negative.py
+++ b/tempest/api/compute/admin/test_servers_negative.py
@@ -61,7 +61,7 @@
flavor_ref = self.create_flavor(ram=ram, vcpus=vcpus, disk=disk)
self.assertRaises((lib_exc.Forbidden, lib_exc.OverLimit),
self.client.resize_server,
- self.servers[0]['id'],
+ self.s1_id,
flavor_ref['id'])
@decorators.idempotent_id('7368a427-2f26-4ad9-9ba9-911a0ec2b0db')
@@ -83,7 +83,7 @@
flavor_ref = self.create_flavor(ram=ram, vcpus=vcpus, disk=disk)
self.assertRaises((lib_exc.Forbidden, lib_exc.OverLimit),
self.client.resize_server,
- self.servers[0]['id'],
+ self.s1_id,
flavor_ref['id'])
@decorators.attr(type=['negative'])
diff --git a/tempest/api/compute/base.py b/tempest/api/compute/base.py
index 3340d8c..ac03cdc 100644
--- a/tempest/api/compute/base.py
+++ b/tempest/api/compute/base.py
@@ -116,42 +116,6 @@
cls.ssh_user = CONF.validation.image_ssh_user
cls.image_ssh_user = CONF.validation.image_ssh_user
cls.image_ssh_password = CONF.validation.image_ssh_password
- cls.servers = []
- cls.images = []
- cls.security_groups = []
- cls.server_groups = []
- cls.volumes = []
-
- @classmethod
- def resource_cleanup(cls):
- cls.clear_resources('images', cls.images,
- cls.compute_images_client.delete_image)
- cls.clear_servers()
- cls.clear_resources('security groups', cls.security_groups,
- cls.security_groups_client.delete_security_group)
- cls.clear_resources('server groups', cls.server_groups,
- cls.server_groups_client.delete_server_group)
- cls.clear_volumes()
- super(BaseV2ComputeTest, cls).resource_cleanup()
-
- @classmethod
- def clear_servers(cls):
- LOG.debug('Clearing servers: %s', ','.join(
- server['id'] for server in cls.servers))
- for server in cls.servers:
- try:
- test_utils.call_and_ignore_notfound_exc(
- cls.servers_client.delete_server, server['id'])
- except Exception:
- LOG.exception('Deleting server %s failed', server['id'])
-
- for server in cls.servers:
- try:
- waiters.wait_for_server_termination(cls.servers_client,
- server['id'])
- except Exception:
- LOG.exception('Waiting for deletion of server %s failed',
- server['id'])
@classmethod
def server_check_teardown(cls):
@@ -190,7 +154,7 @@
@classmethod
def create_test_server(cls, validatable=False, volume_backed=False,
- **kwargs):
+ validation_resources=None, **kwargs):
"""Wrapper utility that returns a test server.
This wrapper utility calls the common create test server and
@@ -200,6 +164,10 @@
:param validatable: Whether the server will be pingable or sshable.
: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 kwargs: Extra arguments are passed down to the
+ `compute.create_test_server` call.
"""
if 'name' not in kwargs:
kwargs['name'] = data_utils.rand_name(cls.__name__ + "-server")
@@ -216,12 +184,20 @@
body, servers = compute.create_test_server(
cls.os_primary,
validatable,
- validation_resources=cls.validation_resources,
+ validation_resources=validation_resources,
tenant_network=tenant_network,
volume_backed=volume_backed,
**kwargs)
- cls.servers.extend(servers)
+ # For each server schedule wait and delete, so we first delete all
+ # and then wait for all
+ for server in servers:
+ cls.addClassResourceCleanup(waiters.wait_for_server_termination,
+ cls.servers_client, server['id'])
+ for server in servers:
+ cls.addClassResourceCleanup(
+ test_utils.call_and_ignore_notfound_exc,
+ cls.servers_client.delete_server, server['id'])
return body
@@ -233,7 +209,10 @@
description = data_utils.rand_name('description')
body = cls.security_groups_client.create_security_group(
name=name, description=description)['security_group']
- cls.security_groups.append(body['id'])
+ cls.addClassResourceCleanup(
+ test_utils.call_and_ignore_notfound_exc,
+ cls.security_groups_client.delete_security_group,
+ body['id'])
return body
@@ -245,7 +224,10 @@
policy = ['affinity']
body = cls.server_groups_client.create_server_group(
name=name, policies=policy)['server_group']
- cls.server_groups.append(body['id'])
+ cls.addClassResourceCleanup(
+ test_utils.call_and_ignore_notfound_exc,
+ cls.server_groups_client.delete_server_group,
+ body['id'])
return body
def wait_for(self, condition):
@@ -263,18 +245,6 @@
return
time.sleep(self.build_interval)
- @staticmethod
- def _delete_volume(volumes_client, volume_id):
- """Deletes the given volume and waits for it to be gone."""
- try:
- volumes_client.delete_volume(volume_id)
- # TODO(mriedem): We should move the wait_for_resource_deletion
- # into the delete_volume method as a convenience to the caller.
- volumes_client.wait_for_resource_deletion(volume_id)
- except lib_exc.NotFound:
- LOG.warning("Unable to delete volume '%s' since it was not found. "
- "Maybe it was already deleted?", volume_id)
-
@classmethod
def prepare_instance_network(cls):
if (CONF.validation.auth_method != 'disabled' and
@@ -292,8 +262,14 @@
image = cls.compute_images_client.create_image(server_id, name=name,
**kwargs)
- image_id = data_utils.parse_image_id(image.response['location'])
- cls.images.append(image_id)
+ if api_version_utils.compare_version_header_to_response(
+ "OpenStack-API-Version", "compute 2.45", image.response, "lt"):
+ image_id = image['image_id']
+ else:
+ image_id = data_utils.parse_image_id(image.response['location'])
+ cls.addClassResourceCleanup(test_utils.call_and_ignore_notfound_exc,
+ cls.compute_images_client.delete_image,
+ image_id)
if wait_until is not None:
try:
@@ -325,14 +301,34 @@
return image
@classmethod
- def rebuild_server(cls, server_id, validatable=False, **kwargs):
- # Destroy an existing server and creates a new one
+ def recreate_server(cls, server_id, validatable=False, **kwargs):
+ """Destroy an existing class level server and creates a new one
+
+ Some test classes use a test server that can be used by multiple
+ tests. This is done to optimise runtime and test load.
+ If something goes wrong with the test server, it can be rebuilt
+ using this helper.
+
+ This helper can also be used for the initial provisioning if no
+ server_id is specified.
+
+ :param server_id: UUID of the server to be rebuilt. If None is
+ specified, a new server is provisioned.
+ :param validatable: whether to the server needs to be
+ validatable. When True, validation resources are acquired via
+ the `get_class_validation_resources` helper.
+ :param kwargs: extra paramaters are passed through to the
+ `create_test_server` call.
+ :return: the UUID of the created server.
+ """
if server_id:
cls.delete_server(server_id)
cls.password = data_utils.rand_password()
server = cls.create_test_server(
validatable,
+ validation_resources=cls.get_class_validation_resources(
+ cls.os_primary),
wait_until='ACTIVE',
adminPass=cls.password,
**kwargs)
@@ -360,17 +356,33 @@
@classmethod
def delete_volume(cls, volume_id):
"""Deletes the given volume and waits for it to be gone."""
- cls._delete_volume(cls.volumes_client, volume_id)
+ try:
+ cls.volumes_client.delete_volume(volume_id)
+ # TODO(mriedem): We should move the wait_for_resource_deletion
+ # into the delete_volume method as a convenience to the caller.
+ cls.volumes_client.wait_for_resource_deletion(volume_id)
+ except lib_exc.NotFound:
+ LOG.warning("Unable to delete volume '%s' since it was not found. "
+ "Maybe it was already deleted?", volume_id)
@classmethod
- def get_server_ip(cls, server):
+ def get_server_ip(cls, server, validation_resources=None):
"""Get the server fixed or floating IP.
Based on the configuration we're in, return a correct ip
address for validating that a guest is up.
+
+ :param server: The server dict as returned by the API
+ :param validation_resources: The dict of validation resources
+ provisioned for the server.
"""
if CONF.validation.connect_method == 'floating':
- return cls.validation_resources['floating_ip']['ip']
+ if validation_resources:
+ return validation_resources['floating_ip']['ip']
+ else:
+ msg = ('When validation.connect_method equals floating, '
+ 'validation_resources cannot be None')
+ raise exceptions.InvalidParam(invalid_param=msg)
elif CONF.validation.connect_method == 'fixed':
addresses = server['addresses'][CONF.validation.network_for_ssh]
for address in addresses:
@@ -401,30 +413,33 @@
if image_ref is not None:
kwargs['imageRef'] = image_ref
volume = cls.volumes_client.create_volume(**kwargs)['volume']
- cls.volumes.append(volume)
+ cls.addClassResourceCleanup(
+ cls.volumes_client.wait_for_resource_deletion, volume['id'])
+ cls.addClassResourceCleanup(test_utils.call_and_ignore_notfound_exc,
+ cls.volumes_client.delete_volume,
+ volume['id'])
waiters.wait_for_volume_resource_status(cls.volumes_client,
volume['id'], 'available')
return volume
- @classmethod
- def clear_volumes(cls):
- LOG.debug('Clearing volumes: %s', ','.join(
- volume['id'] for volume in cls.volumes))
- for volume in cls.volumes:
- try:
- test_utils.call_and_ignore_notfound_exc(
- cls.volumes_client.delete_volume, volume['id'])
- except Exception:
- LOG.exception('Deleting volume %s failed', volume['id'])
+ def _detach_volume(self, server, volume):
+ """Helper method to detach a volume.
- for volume in cls.volumes:
- try:
- cls.volumes_client.wait_for_resource_deletion(volume['id'])
- except Exception:
- LOG.exception('Waiting for deletion of volume %s failed',
- volume['id'])
+ Ignores 404 responses if the volume or server do not exist, or the
+ volume is already detached from the server.
+ """
+ try:
+ volume = self.volumes_client.show_volume(volume['id'])['volume']
+ # Check the status. You can only detach an in-use volume, otherwise
+ # the compute API will return a 400 response.
+ if volume['status'] == 'in-use':
+ self.servers_client.detach_volume(server['id'], volume['id'])
+ except lib_exc.NotFound:
+ # Ignore 404s on detach in case the server is deleted or the volume
+ # is already detached.
+ pass
- def attach_volume(self, server, volume, device=None, check_reserved=False):
+ def attach_volume(self, server, volume, device=None):
"""Attaches volume to server and waits for 'in-use' volume status.
The volume will be detached when the test tears down.
@@ -433,15 +448,10 @@
:param volume: The volume to attach.
:param device: Optional mountpoint for the attached volume. Note that
this is not guaranteed for all hypervisors and is not recommended.
- :param check_reserved: Consider a status of reserved as valid for
- completion. This is to handle new Cinder attach where we more
- accurately use 'reserved' for things like attaching to a shelved
- server.
"""
attach_kwargs = dict(volumeId=volume['id'])
if device:
attach_kwargs['device'] = device
-
attachment = self.servers_client.attach_volume(
server['id'], **attach_kwargs)['volumeAttachment']
# On teardown detach the volume and wait for it to be available. This
@@ -451,14 +461,9 @@
self.volumes_client, volume['id'], 'available')
# Ignore 404s on detach in case the server is deleted or the volume
# is already detached.
- self.addCleanup(test_utils.call_and_ignore_notfound_exc,
- self.servers_client.detach_volume,
- server['id'], volume['id'])
- statuses = ['in-use']
- if check_reserved:
- statuses.append('reserved')
+ self.addCleanup(self._detach_volume, server, volume)
waiters.wait_for_volume_resource_status(self.volumes_client,
- volume['id'], statuses)
+ volume['id'], 'in-use')
return attachment
diff --git a/tempest/api/compute/flavors/test_flavors.py b/tempest/api/compute/flavors/test_flavors.py
index d5bb45a..20294e9 100644
--- a/tempest/api/compute/flavors/test_flavors.py
+++ b/tempest/api/compute/flavors/test_flavors.py
@@ -18,8 +18,6 @@
class FlavorsV2TestJSON(base.BaseV2ComputeTest):
- _min_disk = 'minDisk'
- _min_ram = 'minRam'
@decorators.attr(type='smoke')
@decorators.idempotent_id('e36c0eaa-dff5-4082-ad1f-3f9a80aa3f59')
@@ -89,7 +87,7 @@
flavor = self.flavors_client.show_flavor(self.flavor_ref)['flavor']
flavor_id = flavor['id']
- params = {self._min_disk: flavor['disk'] + 1}
+ params = {'minDisk': flavor['disk'] + 1}
flavors = self.flavors_client.list_flavors(detail=True,
**params)['flavors']
self.assertEmpty([i for i in flavors if i['id'] == flavor_id])
@@ -100,7 +98,7 @@
flavor = self.flavors_client.show_flavor(self.flavor_ref)['flavor']
flavor_id = flavor['id']
- params = {self._min_ram: flavor['ram'] + 1}
+ params = {'minRam': flavor['ram'] + 1}
flavors = self.flavors_client.list_flavors(detail=True,
**params)['flavors']
self.assertEmpty([i for i in flavors if i['id'] == flavor_id])
@@ -111,7 +109,7 @@
flavor = self.flavors_client.show_flavor(self.flavor_ref)['flavor']
flavor_id = flavor['id']
- params = {self._min_disk: flavor['disk'] + 1}
+ params = {'minDisk': flavor['disk'] + 1}
flavors = self.flavors_client.list_flavors(**params)['flavors']
self.assertEmpty([i for i in flavors if i['id'] == flavor_id])
@@ -121,6 +119,6 @@
flavor = self.flavors_client.show_flavor(self.flavor_ref)['flavor']
flavor_id = flavor['id']
- params = {self._min_ram: flavor['ram'] + 1}
+ params = {'minRam': flavor['ram'] + 1}
flavors = self.flavors_client.list_flavors(**params)['flavors']
self.assertEmpty([i for i in flavors if i['id'] == flavor_id])
diff --git a/tempest/api/compute/floating_ips/base.py b/tempest/api/compute/floating_ips/base.py
index 142eaec..262a3c1 100644
--- a/tempest/api/compute/floating_ips/base.py
+++ b/tempest/api/compute/floating_ips/base.py
@@ -14,6 +14,10 @@
# under the License.
from tempest.api.compute import base
+from tempest.common import utils
+from tempest import config
+
+CONF = config.CONF
class BaseFloatingIPsTest(base.BaseV2ComputeTest):
@@ -24,3 +28,17 @@
cls.set_network_resources(network=True, subnet=True,
router=True, dhcp=True)
super(BaseFloatingIPsTest, cls).setup_credentials()
+
+ @classmethod
+ def skip_checks(cls):
+ super(BaseFloatingIPsTest, cls).skip_checks()
+ if not utils.get_service_list()['network']:
+ raise cls.skipException("network service not enabled.")
+ if not CONF.network_feature_enabled.floating_ips:
+ raise cls.skipException("Floating ips are not available")
+
+ @classmethod
+ def setup_clients(cls):
+ super(BaseFloatingIPsTest, cls).setup_clients()
+ cls.client = cls.floating_ips_client
+ cls.pools_client = cls.floating_ip_pools_client
diff --git a/tempest/api/compute/floating_ips/test_floating_ips_actions.py b/tempest/api/compute/floating_ips/test_floating_ips_actions.py
index 9ee0ac9..2adc482 100644
--- a/tempest/api/compute/floating_ips/test_floating_ips_actions.py
+++ b/tempest/api/compute/floating_ips/test_floating_ips_actions.py
@@ -16,7 +16,6 @@
import testtools
from tempest.api.compute.floating_ips import base
-from tempest.common import utils
from tempest import config
from tempest.lib.common.utils import test_utils
from tempest.lib import decorators
@@ -26,42 +25,8 @@
class FloatingIPsTestJSON(base.BaseFloatingIPsTest):
- server_id = None
- floating_ip = None
- @classmethod
- def skip_checks(cls):
- super(FloatingIPsTestJSON, cls).skip_checks()
- if not utils.get_service_list()['network']:
- raise cls.skipException("network service not enabled.")
- if not CONF.network_feature_enabled.floating_ips:
- raise cls.skipException("Floating ips are not available")
-
- @classmethod
- def setup_clients(cls):
- super(FloatingIPsTestJSON, cls).setup_clients()
- cls.client = cls.floating_ips_client
-
- @classmethod
- def resource_setup(cls):
- super(FloatingIPsTestJSON, cls).resource_setup()
- cls.floating_ip_id = None
-
- # Server creation
- server = cls.create_test_server(wait_until='ACTIVE')
- cls.server_id = server['id']
- # Floating IP creation
- body = cls.client.create_floating_ip(
- pool=CONF.network.floating_network_name)['floating_ip']
- cls.floating_ip_id = body['id']
- cls.floating_ip = body['ip']
-
- @classmethod
- def resource_cleanup(cls):
- # Deleting the floating IP which is created in this method
- if cls.floating_ip_id:
- cls.client.delete_floating_ip(cls.floating_ip_id)
- super(FloatingIPsTestJSON, cls).resource_cleanup()
+ max_microversion = '2.35'
@decorators.idempotent_id('f7bfb946-297e-41b8-9e8c-aba8e9bb5194')
def test_allocate_floating_ip(self):
@@ -92,6 +57,25 @@
# Check it was really deleted.
self.client.wait_for_resource_deletion(floating_ip_body['id'])
+
+class FloatingIPsAssociationTestJSON(base.BaseFloatingIPsTest):
+
+ max_microversion = '2.43'
+
+ @classmethod
+ def resource_setup(cls):
+ super(FloatingIPsAssociationTestJSON, cls).resource_setup()
+
+ # Server creation
+ cls.server = cls.create_test_server(wait_until='ACTIVE')
+ cls.server_id = cls.server['id']
+ # Floating IP creation
+ body = cls.client.create_floating_ip(
+ pool=CONF.network.floating_network_name)['floating_ip']
+ cls.addClassResourceCleanup(cls.client.delete_floating_ip, body['id'])
+ cls.floating_ip_id = body['id']
+ cls.floating_ip = body['ip']
+
@decorators.idempotent_id('307efa27-dc6f-48a0-8cd2-162ce3ef0b52')
@testtools.skipUnless(CONF.network.public_network_id,
'The public_network_id option must be specified.')
diff --git a/tempest/api/compute/floating_ips/test_floating_ips_actions_negative.py b/tempest/api/compute/floating_ips/test_floating_ips_actions_negative.py
index c3d7816..9257458 100644
--- a/tempest/api/compute/floating_ips/test_floating_ips_actions_negative.py
+++ b/tempest/api/compute/floating_ips/test_floating_ips_actions_negative.py
@@ -16,7 +16,6 @@
import testtools
from tempest.api.compute.floating_ips import base
-from tempest.common import utils
from tempest import config
from tempest.lib.common.utils import data_utils
from tempest.lib import decorators
@@ -27,26 +26,12 @@
class FloatingIPsNegativeTestJSON(base.BaseFloatingIPsTest):
- @classmethod
- def skip_checks(cls):
- super(FloatingIPsNegativeTestJSON, cls).skip_checks()
- if not utils.get_service_list()['network']:
- raise cls.skipException("network service not enabled.")
- if not CONF.network_feature_enabled.floating_ips:
- raise cls.skipException("Floating ips are not available")
-
- @classmethod
- def setup_clients(cls):
- super(FloatingIPsNegativeTestJSON, cls).setup_clients()
- cls.client = cls.floating_ips_client
+ max_microversion = '2.35'
@classmethod
def resource_setup(cls):
super(FloatingIPsNegativeTestJSON, cls).resource_setup()
- # Server creation
- server = cls.create_test_server(wait_until='ACTIVE')
- cls.server_id = server['id']
# Generating a nonexistent floatingIP id
body = cls.client.list_floating_ips()['floating_ips']
floating_ip_ids = [floating_ip['id'] for floating_ip in body]
@@ -77,6 +62,17 @@
self.assertRaises(lib_exc.NotFound, self.client.delete_floating_ip,
self.non_exist_id)
+
+class FloatingIPsAssociationNegativeTestJSON(base.BaseFloatingIPsTest):
+
+ max_microversion = '2.43'
+
+ @classmethod
+ def resource_setup(cls):
+ super(FloatingIPsAssociationNegativeTestJSON, cls).resource_setup()
+ cls.server = cls.create_test_server(wait_until='ACTIVE')
+ cls.server_id = cls.server['id']
+
@decorators.attr(type=['negative'])
@decorators.idempotent_id('595fa616-1a71-4670-9614-46564ac49a4c')
def test_associate_nonexistent_floating_ip(self):
diff --git a/tempest/api/compute/floating_ips/test_list_floating_ips.py b/tempest/api/compute/floating_ips/test_list_floating_ips.py
index 2314433..944f798 100644
--- a/tempest/api/compute/floating_ips/test_list_floating_ips.py
+++ b/tempest/api/compute/floating_ips/test_list_floating_ips.py
@@ -13,46 +13,27 @@
# License for the specific language governing permissions and limitations
# under the License.
-from tempest.api.compute import base
-from tempest.common import utils
+from tempest.api.compute.floating_ips import base
from tempest import config
from tempest.lib import decorators
CONF = config.CONF
-class FloatingIPDetailsTestJSON(base.BaseV2ComputeTest):
+class FloatingIPDetailsTestJSON(base.BaseFloatingIPsTest):
- @classmethod
- def skip_checks(cls):
- super(FloatingIPDetailsTestJSON, cls).skip_checks()
- if not utils.get_service_list()['network']:
- raise cls.skipException("network service not enabled.")
- if not CONF.network_feature_enabled.floating_ips:
- raise cls.skipException("Floating ips are not available")
-
- @classmethod
- def setup_clients(cls):
- super(FloatingIPDetailsTestJSON, cls).setup_clients()
- cls.client = cls.floating_ips_client
- cls.pools_client = cls.floating_ip_pools_client
+ max_microversion = '2.35'
@classmethod
def resource_setup(cls):
super(FloatingIPDetailsTestJSON, cls).resource_setup()
cls.floating_ip = []
- cls.floating_ip_id = []
for _ in range(3):
body = cls.client.create_floating_ip(
pool=CONF.network.floating_network_name)['floating_ip']
+ cls.addClassResourceCleanup(cls.client.delete_floating_ip,
+ body['id'])
cls.floating_ip.append(body)
- cls.floating_ip_id.append(body['id'])
-
- @classmethod
- def resource_cleanup(cls):
- for f_id in cls.floating_ip_id:
- cls.client.delete_floating_ip(f_id)
- super(FloatingIPDetailsTestJSON, cls).resource_cleanup()
@decorators.idempotent_id('16db31c3-fb85-40c9-bbe2-8cf7b67ff99f')
def test_list_floating_ips(self):
diff --git a/tempest/api/compute/floating_ips/test_list_floating_ips_negative.py b/tempest/api/compute/floating_ips/test_list_floating_ips_negative.py
index 0ade872..d69248c 100644
--- a/tempest/api/compute/floating_ips/test_list_floating_ips_negative.py
+++ b/tempest/api/compute/floating_ips/test_list_floating_ips_negative.py
@@ -13,8 +13,7 @@
# License for the specific language governing permissions and limitations
# under the License.
-from tempest.api.compute import base
-from tempest.common import utils
+from tempest.api.compute.floating_ips import base
from tempest import config
from tempest.lib.common.utils import data_utils
from tempest.lib import decorators
@@ -23,20 +22,9 @@
CONF = config.CONF
-class FloatingIPDetailsNegativeTestJSON(base.BaseV2ComputeTest):
+class FloatingIPDetailsNegativeTestJSON(base.BaseFloatingIPsTest):
- @classmethod
- def skip_checks(cls):
- super(FloatingIPDetailsNegativeTestJSON, cls).skip_checks()
- if not utils.get_service_list()['network']:
- raise cls.skipException("network service not enabled.")
- if not CONF.network_feature_enabled.floating_ips:
- raise cls.skipException("Floating ips are not available")
-
- @classmethod
- def setup_clients(cls):
- super(FloatingIPDetailsNegativeTestJSON, cls).setup_clients()
- cls.client = cls.floating_ips_client
+ max_microversion = '2.35'
@decorators.attr(type=['negative'])
@decorators.idempotent_id('7ab18834-4a4b-4f28-a2c5-440579866695')
diff --git a/tempest/api/compute/images/test_image_metadata.py b/tempest/api/compute/images/test_image_metadata.py
index 8d503dc..b497626 100644
--- a/tempest/api/compute/images/test_image_metadata.py
+++ b/tempest/api/compute/images/test_image_metadata.py
@@ -20,6 +20,7 @@
from tempest.common import waiters
from tempest import config
from tempest.lib.common.utils import data_utils
+from tempest.lib.common.utils import test_utils
from tempest.lib import decorators
from tempest.lib import exceptions
@@ -70,7 +71,9 @@
body = cls.glance_client.create_image(**params)
body = body['image'] if 'image' in body else body
cls.image_id = body['id']
- cls.images.append(cls.image_id)
+ cls.addClassResourceCleanup(test_utils.call_and_ignore_notfound_exc,
+ cls.glance_client.delete_image,
+ cls.image_id)
image_file = six.BytesIO((b'*' * 1024))
if CONF.image_feature_enabled.api_v1:
cls.glance_client.update_image(cls.image_id, data=image_file)
diff --git a/tempest/api/compute/images/test_images_oneserver.py b/tempest/api/compute/images/test_images_oneserver.py
index 5987d39..058e7e6 100644
--- a/tempest/api/compute/images/test_images_oneserver.py
+++ b/tempest/api/compute/images/test_images_oneserver.py
@@ -15,6 +15,7 @@
from tempest.api.compute import base
from tempest import config
+from tempest.lib.common import api_version_utils
from tempest.lib.common.utils import data_utils
from tempest.lib import decorators
@@ -74,7 +75,6 @@
# Verify the image was deleted correctly
self.client.delete_image(image['id'])
- self.images.remove(image['id'])
self.client.wait_for_resource_deletion(image['id'])
@decorators.idempotent_id('3b7c6fe4-dfe7-477c-9243-b06359db51e6')
@@ -87,5 +87,9 @@
# 4 byte utf-8 character.
utf8_name = data_utils.rand_name(b'\xe2\x82\xa1'.decode('utf-8'))
body = self.client.create_image(self.server_id, name=utf8_name)
- image_id = data_utils.parse_image_id(body.response['location'])
+ if api_version_utils.compare_version_header_to_response(
+ "OpenStack-API-Version", "compute 2.45", body.response, "lt"):
+ image_id = body['image_id']
+ else:
+ image_id = data_utils.parse_image_id(body.response['location'])
self.addCleanup(self.client.delete_image, image_id)
diff --git a/tempest/api/compute/images/test_images_oneserver_negative.py b/tempest/api/compute/images/test_images_oneserver_negative.py
index cf32ba3..a2e58c9 100644
--- a/tempest/api/compute/images/test_images_oneserver_negative.py
+++ b/tempest/api/compute/images/test_images_oneserver_negative.py
@@ -19,6 +19,7 @@
from tempest.api.compute import base
from tempest.common import waiters
from tempest import config
+from tempest.lib.common import api_version_utils
from tempest.lib.common.utils import data_utils
from tempest.lib import decorators
from tempest.lib import exceptions as lib_exc
@@ -51,7 +52,7 @@
self._reset_server()
def _reset_server(self):
- self.__class__.server_id = self.rebuild_server(self.server_id)
+ self.__class__.server_id = self.recreate_server(self.server_id)
@classmethod
def skip_checks(cls):
@@ -105,9 +106,12 @@
self.assertRaises(lib_exc.Conflict, self.create_image_from_server,
self.server_id)
- image_id = data_utils.parse_image_id(image.response['location'])
+ if api_version_utils.compare_version_header_to_response(
+ "OpenStack-API-Version", "compute 2.45", image.response, "lt"):
+ image_id = image['image_id']
+ else:
+ image_id = data_utils.parse_image_id(image.response['location'])
self.client.delete_image(image_id)
- self.images.remove(image_id)
@decorators.attr(type=['negative'])
@decorators.idempotent_id('084f0cbc-500a-4963-8a4e-312905862581')
@@ -124,12 +128,15 @@
# Return an error while trying to delete an image what is creating
image = self.create_image_from_server(self.server_id)
- image_id = data_utils.parse_image_id(image.response['location'])
+ if api_version_utils.compare_version_header_to_response(
+ "OpenStack-API-Version", "compute 2.45", image.response, "lt"):
+ image_id = image['image_id']
+ else:
+ image_id = data_utils.parse_image_id(image.response['location'])
self.addCleanup(self._reset_server)
# Do not wait, attempt to delete the image, ensure it's successful
self.client.delete_image(image_id)
- self.images.remove(image_id)
self.assertRaises(lib_exc.NotFound,
self.client.show_image, image_id)
diff --git a/tempest/api/compute/images/test_list_image_filters.py b/tempest/api/compute/images/test_list_image_filters.py
index acc8b3e..d83d8df 100644
--- a/tempest/api/compute/images/test_list_image_filters.py
+++ b/tempest/api/compute/images/test_list_image_filters.py
@@ -23,6 +23,7 @@
from tempest.common import waiters
from tempest import config
from tempest.lib.common.utils import data_utils
+from tempest.lib.common.utils import test_utils
from tempest.lib import decorators
from tempest.lib import exceptions
@@ -74,7 +75,10 @@
body = cls.glance_client.create_image(**params)
body = body['image'] if 'image' in body else body
image_id = body['id']
- cls.images.append(image_id)
+ cls.addClassResourceCleanup(
+ test_utils.call_and_ignore_notfound_exc,
+ cls.compute_images_client.delete_image,
+ image_id)
# Wait 1 second between creation and upload to ensure a delta
# between created_at and updated_at.
time.sleep(1)
diff --git a/tempest/api/compute/servers/test_attach_interfaces.py b/tempest/api/compute/servers/test_attach_interfaces.py
index 0248c65..0e8f681 100644
--- a/tempest/api/compute/servers/test_attach_interfaces.py
+++ b/tempest/api/compute/servers/test_attach_interfaces.py
@@ -78,8 +78,11 @@
return port
- def _check_interface(self, iface, port_id=None, network_id=None,
- fixed_ip=None, mac_addr=None):
+ def _check_interface(self, iface, server_id=None, port_id=None,
+ network_id=None, fixed_ip=None, mac_addr=None):
+ if server_id:
+ iface = waiters.wait_for_interface_status(
+ self.interfaces_client, server_id, iface['port_id'], 'ACTIVE')
if port_id:
self.assertEqual(iface['port_id'], port_id)
if network_id:
@@ -109,9 +112,8 @@
network_id = ifs[0]['net_id']
iface = self.interfaces_client.create_interface(
server['id'], net_id=network_id)['interfaceAttachment']
- iface = waiters.wait_for_interface_status(
- self.interfaces_client, server['id'], iface['port_id'], 'ACTIVE')
- self._check_interface(iface, network_id=network_id)
+ self._check_interface(iface, server_id=server['id'],
+ network_id=network_id)
return iface
def _test_create_interface_by_port_id(self, server, ifs):
@@ -121,9 +123,8 @@
self.addCleanup(self.ports_client.delete_port, port_id)
iface = self.interfaces_client.create_interface(
server['id'], port_id=port_id)['interfaceAttachment']
- iface = waiters.wait_for_interface_status(
- self.interfaces_client, server['id'], iface['port_id'], 'ACTIVE')
- self._check_interface(iface, port_id=port_id)
+ self._check_interface(iface, server_id=server['id'], port_id=port_id,
+ network_id=network_id)
return iface
def _test_create_interface_by_fixed_ips(self, server, ifs):
@@ -140,9 +141,8 @@
server['id'], net_id=network_id,
fixed_ips=fixed_ips)['interfaceAttachment']
self.addCleanup(self.ports_client.delete_port, iface['port_id'])
- iface = waiters.wait_for_interface_status(
- self.interfaces_client, server['id'], iface['port_id'], 'ACTIVE')
- self._check_interface(iface, fixed_ip=ip_list[0])
+ self._check_interface(iface, server_id=server['id'],
+ fixed_ip=ip_list[0])
return iface
def _test_show_interface(self, server, ifs):
@@ -271,7 +271,8 @@
# attach the port to the server
iface = self.interfaces_client.create_interface(
server['id'], port_id=port_id)['interfaceAttachment']
- self._check_interface(iface, port_id=port_id)
+ self._check_interface(iface, server_id=server['id'],
+ port_id=port_id)
# detach the port from the server; this is a cast in the compute
# API so we have to poll the port until the device_id is unset.
diff --git a/tempest/api/compute/servers/test_create_server.py b/tempest/api/compute/servers/test_create_server.py
index d8ce7ea..c660821 100644
--- a/tempest/api/compute/servers/test_create_server.py
+++ b/tempest/api/compute/servers/test_create_server.py
@@ -42,8 +42,9 @@
@classmethod
def resource_setup(cls):
- cls.set_validation_resources()
super(ServersTestJSON, cls).resource_setup()
+ validation_resources = cls.get_class_validation_resources(
+ cls.os_primary)
cls.meta = {'hello': 'world'}
cls.accessIPv4 = '1.1.1.1'
cls.accessIPv6 = '0000:0000:0000:0000:0000:babe:220.12.22.2'
@@ -52,6 +53,7 @@
disk_config = cls.disk_config
server_initial = cls.create_test_server(
validatable=True,
+ validation_resources=validation_resources,
wait_until='ACTIVE',
name=cls.name,
metadata=cls.meta,
@@ -105,11 +107,13 @@
# Verify that the number of vcpus reported by the instance matches
# the amount stated by the flavor
flavor = self.flavors_client.show_flavor(self.flavor_ref)['flavor']
+ validation_resources = self.get_class_validation_resources(
+ self.os_primary)
linux_client = remote_client.RemoteClient(
- self.get_server_ip(self.server),
+ self.get_server_ip(self.server, validation_resources),
self.ssh_user,
self.password,
- self.validation_resources['keypair']['private_key'],
+ validation_resources['keypair']['private_key'],
server=self.server,
servers_client=self.client)
output = linux_client.exec_command('grep -c ^processor /proc/cpuinfo')
@@ -120,11 +124,13 @@
'Instance validation tests are disabled.')
def test_host_name_is_same_as_server_name(self):
# Verify the instance host name is the same as the server name
+ validation_resources = self.get_class_validation_resources(
+ self.os_primary)
linux_client = remote_client.RemoteClient(
- self.get_server_ip(self.server),
+ self.get_server_ip(self.server, validation_resources),
self.ssh_user,
self.password,
- self.validation_resources['keypair']['private_key'],
+ validation_resources['keypair']['private_key'],
server=self.server,
servers_client=self.client)
hostname = linux_client.exec_command("hostname").rstrip()
diff --git a/tempest/api/compute/servers/test_device_tagging.py b/tempest/api/compute/servers/test_device_tagging.py
index dbf6713..a126fd6 100644
--- a/tempest/api/compute/servers/test_device_tagging.py
+++ b/tempest/api/compute/servers/test_device_tagging.py
@@ -66,11 +66,6 @@
dhcp=True)
super(DeviceTaggingTest, cls).setup_credentials()
- @classmethod
- def resource_setup(cls):
- cls.set_validation_resources()
- super(DeviceTaggingTest, cls).resource_setup()
-
def verify_device_metadata(self, md_json):
md_dict = json.loads(md_json)
for d in md_dict['devices']:
@@ -139,9 +134,12 @@
# Create server
admin_pass = data_utils.rand_password()
config_drive_enabled = CONF.compute_feature_enabled.config_drive
+ validation_resources = self.get_test_validation_resources(
+ self.os_primary)
server = self.create_test_server(
validatable=True,
+ validation_resources=validation_resources,
config_drive=config_drive_enabled,
adminPass=admin_pass,
name=data_utils.rand_name('device-tagging-server'),
@@ -208,10 +206,10 @@
self.addCleanup(self.delete_server, server['id'])
self.ssh_client = remote_client.RemoteClient(
- self.get_server_ip(server),
+ self.get_server_ip(server, validation_resources),
CONF.validation.image_ssh_user,
admin_pass,
- self.validation_resources['keypair']['private_key'],
+ validation_resources['keypair']['private_key'],
server=server,
servers_client=self.servers_client)
diff --git a/tempest/api/compute/servers/test_server_actions.py b/tempest/api/compute/servers/test_server_actions.py
index b5fc39c..e2be249 100644
--- a/tempest/api/compute/servers/test_server_actions.py
+++ b/tempest/api/compute/servers/test_server_actions.py
@@ -44,13 +44,18 @@
self.server_id, 'ACTIVE')
except lib_exc.NotFound:
# The server was deleted by previous test, create a new one
+ # Use class level validation resources to avoid them being
+ # deleted once a test is over
+ validation_resources = self.get_class_validation_resources(
+ self.os_primary)
server = self.create_test_server(
validatable=True,
+ validation_resources=validation_resources,
wait_until='ACTIVE')
self.__class__.server_id = server['id']
except Exception:
# Rebuild server if something happened to it during a test
- self.__class__.server_id = self.rebuild_server(
+ self.__class__.server_id = self.recreate_server(
self.server_id, validatable=True)
def tearDown(self):
@@ -69,10 +74,8 @@
@classmethod
def resource_setup(cls):
- cls.set_validation_resources()
-
super(ServerActionsTestJSON, cls).resource_setup()
- cls.server_id = cls.rebuild_server(None, validatable=True)
+ cls.server_id = cls.recreate_server(None, validatable=True)
@decorators.idempotent_id('6158df09-4b82-4ab3-af6d-29cf36af858d')
@testtools.skipUnless(CONF.compute_feature_enabled.change_password,
@@ -80,8 +83,11 @@
def test_change_server_password(self):
# Since this test messes with the password and makes the
# server unreachable, it should create its own server
+ validation_resources = self.get_test_validation_resources(
+ self.os_primary)
newserver = self.create_test_server(
validatable=True,
+ validation_resources=validation_resources,
wait_until='ACTIVE')
# The server's password should be set to the provided password
new_password = 'Newpass1234'
@@ -92,7 +98,7 @@
# Verify that the user can authenticate with the new password
server = self.client.show_server(newserver['id'])['server']
linux_client = remote_client.RemoteClient(
- self.get_server_ip(server),
+ self.get_server_ip(server, validation_resources),
self.ssh_user,
new_password,
server=server,
@@ -101,13 +107,15 @@
def _test_reboot_server(self, reboot_type):
if CONF.validation.run_validation:
+ validation_resources = self.get_class_validation_resources(
+ self.os_primary)
# Get the time the server was last rebooted,
server = self.client.show_server(self.server_id)['server']
linux_client = remote_client.RemoteClient(
- self.get_server_ip(server),
+ self.get_server_ip(server, validation_resources),
self.ssh_user,
self.password,
- self.validation_resources['keypair']['private_key'],
+ validation_resources['keypair']['private_key'],
server=server,
servers_client=self.client)
boot_time = linux_client.get_boot_time()
@@ -122,10 +130,10 @@
if CONF.validation.run_validation:
# Log in and verify the boot time has changed
linux_client = remote_client.RemoteClient(
- self.get_server_ip(server),
+ self.get_server_ip(server, validation_resources),
self.ssh_user,
self.password,
- self.validation_resources['keypair']['private_key'],
+ validation_resources['keypair']['private_key'],
server=server,
servers_client=self.client)
new_boot_time = linux_client.get_boot_time()
@@ -201,6 +209,8 @@
self.assertEqual(original_addresses, server['addresses'])
if CONF.validation.run_validation:
+ validation_resources = self.get_class_validation_resources(
+ self.os_primary)
# Authentication is attempted in the following order of priority:
# 1.The key passed in, if one was passed in.
# 2.Any key we can find through an SSH agent (if allowed).
@@ -208,10 +218,10 @@
# ~/.ssh/ (if allowed).
# 4.Plain username/password auth, if a password was given.
linux_client = remote_client.RemoteClient(
- self.get_server_ip(rebuilt_server),
+ self.get_server_ip(rebuilt_server, validation_resources),
self.ssh_user,
password,
- self.validation_resources['keypair']['private_key'],
+ validation_resources['keypair']['private_key'],
server=rebuilt_server,
servers_client=self.client)
linux_client.validate_authentication()
@@ -271,45 +281,73 @@
self.assertEqual(self.server_id,
vol_after_rebuild['attachments'][0]['server_id'])
- def _test_resize_server_confirm(self, stop=False):
+ def _test_resize_server_confirm(self, server_id, stop=False):
# The server's RAM and disk space should be modified to that of
# the provided flavor
if stop:
- self.client.stop_server(self.server_id)
- waiters.wait_for_server_status(self.client, self.server_id,
+ self.client.stop_server(server_id)
+ waiters.wait_for_server_status(self.client, server_id,
'SHUTOFF')
- self.client.resize_server(self.server_id, self.flavor_ref_alt)
+ self.client.resize_server(server_id, self.flavor_ref_alt)
# NOTE(jlk): Explicitly delete the server to get a new one for later
# tests. Avoids resize down race issues.
- self.addCleanup(self.delete_server, self.server_id)
- waiters.wait_for_server_status(self.client, self.server_id,
+ self.addCleanup(self.delete_server, server_id)
+ waiters.wait_for_server_status(self.client, server_id,
'VERIFY_RESIZE')
- self.client.confirm_resize_server(self.server_id)
+ self.client.confirm_resize_server(server_id)
expected_status = 'SHUTOFF' if stop else 'ACTIVE'
- waiters.wait_for_server_status(self.client, self.server_id,
+ waiters.wait_for_server_status(self.client, server_id,
expected_status)
- server = self.client.show_server(self.server_id)['server']
+ server = self.client.show_server(server_id)['server']
self.assertEqual(self.flavor_ref_alt, server['flavor']['id'])
if stop:
# NOTE(mriedem): tearDown requires the server to be started.
- self.client.start_server(self.server_id)
+ self.client.start_server(server_id)
@decorators.idempotent_id('1499262a-9328-4eda-9068-db1ac57498d2')
@testtools.skipUnless(CONF.compute_feature_enabled.resize,
'Resize not available.')
def test_resize_server_confirm(self):
- self._test_resize_server_confirm(stop=False)
+ self._test_resize_server_confirm(self.server_id, stop=False)
+
+ @decorators.idempotent_id('e6c28180-7454-4b59-b188-0257af08a63b')
+ @decorators.related_bug('1728603')
+ @testtools.skipUnless(CONF.compute_feature_enabled.resize,
+ 'Resize not available.')
+ @utils.services('volume')
+ def test_resize_volume_backed_server_confirm(self):
+ # We have to create a new server that is volume-backed since the one
+ # from setUp is not volume-backed.
+ server = self.create_test_server(
+ volume_backed=True, wait_until='ACTIVE')
+ self._test_resize_server_confirm(server['id'])
+ if CONF.compute_feature_enabled.console_output:
+ # Now do something interactive with the guest like get its console
+ # output; we don't actually care about the output,
+ # just that it doesn't raise an error.
+ self.client.get_console_output(server['id'])
+ if CONF.validation.run_validation:
+ validation_resources = self.get_class_validation_resources(
+ self.os_primary)
+ linux_client = remote_client.RemoteClient(
+ self.get_server_ip(server, validation_resources),
+ self.ssh_user,
+ password=None,
+ pkey=validation_resources['keypair']['private_key'],
+ server=server,
+ servers_client=self.client)
+ linux_client.validate_authentication()
@decorators.idempotent_id('138b131d-66df-48c9-a171-64f45eb92962')
@testtools.skipUnless(CONF.compute_feature_enabled.resize,
'Resize not available.')
def test_resize_server_confirm_from_stopped(self):
- self._test_resize_server_confirm(stop=True)
+ self._test_resize_server_confirm(self.server_id, stop=True)
@decorators.idempotent_id('c03aab19-adb1-44f5-917d-c419577e9e68')
@testtools.skipUnless(CONF.compute_feature_enabled.resize,
diff --git a/tempest/api/compute/servers/test_server_personality.py b/tempest/api/compute/servers/test_server_personality.py
index 90b9da4..6f32b46 100644
--- a/tempest/api/compute/servers/test_server_personality.py
+++ b/tempest/api/compute/servers/test_server_personality.py
@@ -20,6 +20,7 @@
from tempest.common import waiters
from tempest import config
from tempest.lib.common.utils import data_utils
+from tempest.lib.common.utils import test_utils
from tempest.lib import decorators
from tempest.lib import exceptions as lib_exc
@@ -34,11 +35,6 @@
super(ServerPersonalityTestJSON, cls).setup_credentials()
@classmethod
- def resource_setup(cls):
- cls.set_validation_resources()
- super(ServerPersonalityTestJSON, cls).resource_setup()
-
- @classmethod
def skip_checks(cls):
super(ServerPersonalityTestJSON, cls).skip_checks()
if not CONF.compute_feature_enabled.personality:
@@ -48,7 +44,6 @@
def setup_clients(cls):
super(ServerPersonalityTestJSON, cls).setup_clients()
cls.client = cls.servers_client
- cls.user_client = cls.limits_client
@decorators.idempotent_id('3cfe87fd-115b-4a02-b942-7dc36a337fdf')
def test_create_server_with_personality(self):
@@ -57,16 +52,23 @@
personality = [{'path': file_path,
'contents': base64.encode_as_text(file_contents)}]
password = data_utils.rand_password()
- created_server = self.create_test_server(personality=personality,
- adminPass=password,
- wait_until='ACTIVE',
- validatable=True)
+ validation_resources = self.get_test_validation_resources(
+ self.os_primary)
+ created_server = self.create_test_server(
+ personality=personality, adminPass=password, wait_until='ACTIVE',
+ validatable=True,
+ validation_resources=validation_resources)
+ self.addCleanup(waiters.wait_for_server_termination,
+ self.servers_client, created_server['id'])
+ self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+ self.servers_client.delete_server,
+ created_server['id'])
server = self.client.show_server(created_server['id'])['server']
if CONF.validation.run_validation:
linux_client = remote_client.RemoteClient(
- self.get_server_ip(server),
+ self.get_server_ip(server, validation_resources),
self.ssh_user, password,
- self.validation_resources['keypair']['private_key'],
+ validation_resources['keypair']['private_key'],
server=server,
servers_client=self.client)
self.assertEqual(file_contents,
@@ -75,8 +77,16 @@
@decorators.idempotent_id('128966d8-71fc-443c-8cab-08e24114ecc9')
def test_rebuild_server_with_personality(self):
- server = self.create_test_server(wait_until='ACTIVE', validatable=True)
+ validation_resources = self.get_test_validation_resources(
+ self.os_primary)
+ server = self.create_test_server(
+ wait_until='ACTIVE', validatable=True,
+ validation_resources=validation_resources)
server_id = server['id']
+ self.addCleanup(waiters.wait_for_server_termination,
+ self.servers_client, server_id)
+ self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+ self.servers_client.delete_server, server_id)
file_contents = 'Test server rebuild.'
personality = [{'path': 'rebuild.txt',
'contents': base64.encode_as_text(file_contents)}]
@@ -93,7 +103,7 @@
# number of files are injected into the server.
file_contents = 'This is a test file.'
personality = []
- limits = self.user_client.show_limits()['limits']
+ limits = self.limits_client.show_limits()['limits']
max_file_limit = limits['absolute']['maxPersonality']
if max_file_limit == -1:
raise self.skipException("No limit for personality files")
@@ -112,7 +122,7 @@
# Server should be created successfully if maximum allowed number of
# files is injected into the server during creation.
file_contents = 'This is a test file.'
- limits = self.user_client.show_limits()['limits']
+ limits = self.limits_client.show_limits()['limits']
max_file_limit = limits['absolute']['maxPersonality']
if max_file_limit == -1:
raise self.skipException("No limit for personality files")
@@ -126,16 +136,22 @@
'contents': base64.encode_as_text(file_contents + str(i)),
})
password = data_utils.rand_password()
- created_server = self.create_test_server(personality=person,
- adminPass=password,
- wait_until='ACTIVE',
- validatable=True)
+ validation_resources = self.get_test_validation_resources(
+ self.os_primary)
+ created_server = self.create_test_server(
+ personality=person, adminPass=password, wait_until='ACTIVE',
+ validatable=True, validation_resources=validation_resources)
+ self.addCleanup(waiters.wait_for_server_termination,
+ self.servers_client, created_server['id'])
+ self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+ self.servers_client.delete_server,
+ created_server['id'])
server = self.client.show_server(created_server['id'])['server']
if CONF.validation.run_validation:
linux_client = remote_client.RemoteClient(
- self.get_server_ip(server),
+ self.get_server_ip(server, validation_resources),
self.ssh_user, password,
- self.validation_resources['keypair']['private_key'],
+ validation_resources['keypair']['private_key'],
server=server,
servers_client=self.client)
for i in person:
diff --git a/tempest/api/compute/servers/test_servers.py b/tempest/api/compute/servers/test_servers.py
index 7fd1dd1..c9ee671 100644
--- a/tempest/api/compute/servers/test_servers.py
+++ b/tempest/api/compute/servers/test_servers.py
@@ -19,6 +19,7 @@
from tempest.common import waiters
from tempest import config
from tempest.lib.common.utils import data_utils
+from tempest.lib.common.utils import test_utils
from tempest.lib import decorators
CONF = config.CONF
@@ -31,10 +32,6 @@
super(ServersTestJSON, cls).setup_clients()
cls.client = cls.servers_client
- def tearDown(self):
- self.clear_servers()
- super(ServersTestJSON, self).tearDown()
-
@decorators.idempotent_id('b92d5ec7-b1dd-44a2-87e4-45e888c46ef0')
@testtools.skipUnless(CONF.compute_feature_enabled.
enable_instance_password,
@@ -43,6 +40,11 @@
# If an admin password is provided on server creation, the server's
# root password should be set to that password.
server = self.create_test_server(adminPass='testpassword')
+ self.addCleanup(waiters.wait_for_server_termination,
+ self.servers_client, server['id'])
+ self.addCleanup(
+ test_utils.call_and_ignore_notfound_exc,
+ self.servers_client.delete_server, server['id'])
# Verify the password is set correctly in the response
self.assertEqual('testpassword', server['adminPass'])
@@ -57,9 +59,19 @@
server = self.create_test_server(name=server_name,
wait_until='ACTIVE')
id1 = server['id']
+ self.addCleanup(waiters.wait_for_server_termination,
+ self.servers_client, id1)
+ self.addCleanup(
+ test_utils.call_and_ignore_notfound_exc,
+ self.servers_client.delete_server, id1)
server = self.create_test_server(name=server_name,
wait_until='ACTIVE')
id2 = server['id']
+ self.addCleanup(waiters.wait_for_server_termination,
+ self.servers_client, id2)
+ self.addCleanup(
+ test_utils.call_and_ignore_notfound_exc,
+ self.servers_client.delete_server, id2)
self.assertNotEqual(id1, id2, "Did not create a new server")
server = self.client.show_server(id1)['server']
name1 = server['name']
@@ -76,6 +88,11 @@
self.addCleanup(self.keypairs_client.delete_keypair, key_name)
self.keypairs_client.list_keypairs()
server = self.create_test_server(key_name=key_name)
+ self.addCleanup(waiters.wait_for_server_termination,
+ self.servers_client, server['id'])
+ self.addCleanup(
+ test_utils.call_and_ignore_notfound_exc,
+ self.servers_client.delete_server, server['id'])
waiters.wait_for_server_status(self.client, server['id'], 'ACTIVE')
server = self.client.show_server(server['id'])['server']
self.assertEqual(key_name, server['key_name'])
@@ -98,6 +115,11 @@
def test_update_server_name(self):
# The server name should be changed to the provided value
server = self.create_test_server(wait_until='ACTIVE')
+ self.addCleanup(waiters.wait_for_server_termination,
+ self.servers_client, server['id'])
+ self.addCleanup(
+ test_utils.call_and_ignore_notfound_exc,
+ self.servers_client.delete_server, server['id'])
# Update instance name with non-ASCII characters
prefix_name = u'\u00CD\u00F1st\u00E1\u00F1c\u00E9'
self._update_server_name(server['id'], 'ACTIVE', prefix_name)
@@ -115,6 +137,11 @@
def test_update_access_server_address(self):
# The server's access addresses should reflect the provided values
server = self.create_test_server(wait_until='ACTIVE')
+ self.addCleanup(waiters.wait_for_server_termination,
+ self.servers_client, server['id'])
+ self.addCleanup(
+ test_utils.call_and_ignore_notfound_exc,
+ self.servers_client.delete_server, server['id'])
# Update the IPv4 and IPv6 access addresses
self.client.update_server(server['id'],
@@ -131,6 +158,11 @@
def test_create_server_with_ipv6_addr_only(self):
# Create a server without an IPv4 address(only IPv6 address).
server = self.create_test_server(accessIPv6='2001:2001::3')
+ self.addCleanup(waiters.wait_for_server_termination,
+ self.servers_client, server['id'])
+ self.addCleanup(
+ test_utils.call_and_ignore_notfound_exc,
+ self.servers_client.delete_server, server['id'])
waiters.wait_for_server_status(self.client, server['id'], 'ACTIVE')
server = self.client.show_server(server['id'])['server']
self.assertEqual('2001:2001::3', server['accessIPv6'])
diff --git a/tempest/api/compute/servers/test_servers_negative.py b/tempest/api/compute/servers/test_servers_negative.py
index 8170b28..d067bb3 100644
--- a/tempest/api/compute/servers/test_servers_negative.py
+++ b/tempest/api/compute/servers/test_servers_negative.py
@@ -37,7 +37,7 @@
waiters.wait_for_server_status(self.client, self.server_id,
'ACTIVE')
except Exception:
- self.__class__.server_id = self.rebuild_server(self.server_id)
+ self.__class__.server_id = self.recreate_server(self.server_id)
def tearDown(self):
self.server_check_teardown()
@@ -551,7 +551,7 @@
waiters.wait_for_server_status(self.servers_client, self.server_id,
'ACTIVE')
except Exception:
- self.__class__.server_id = self.rebuild_server(self.server_id)
+ self.__class__.server_id = self.recreate_server(self.server_id)
@classmethod
def setup_clients(cls):
diff --git a/tempest/api/compute/volumes/test_attach_volume.py b/tempest/api/compute/volumes/test_attach_volume.py
index e0fed58..297e8a8 100644
--- a/tempest/api/compute/volumes/test_attach_volume.py
+++ b/tempest/api/compute/volumes/test_attach_volume.py
@@ -40,35 +40,37 @@
@classmethod
def resource_setup(cls):
- cls.set_validation_resources()
super(AttachVolumeTestJSON, cls).resource_setup()
cls.device = CONF.compute.volume_device_name
def _create_server(self):
# Start a server and wait for it to become ready
+ validation_resources = self.get_test_validation_resources(
+ self.os_primary)
server = self.create_test_server(
validatable=True,
+ validation_resources=validation_resources,
wait_until='ACTIVE',
adminPass=self.image_ssh_password)
self.addCleanup(self.delete_server, server['id'])
# Record addresses so that we can ssh later
server['addresses'] = self.servers_client.list_addresses(
server['id'])['addresses']
- return server
+ return server, validation_resources
@decorators.idempotent_id('52e9045a-e90d-4c0d-9087-79d657faffff')
def test_attach_detach_volume(self):
# Stop and Start a server with an attached volume, ensuring that
# the volume remains attached.
- server = self._create_server()
+ server, validation_resources = self._create_server()
# NOTE(andreaf) Create one remote client used throughout the test.
if CONF.validation.run_validation:
linux_client = remote_client.RemoteClient(
- self.get_server_ip(server),
+ self.get_server_ip(server, validation_resources),
self.image_ssh_user,
self.image_ssh_password,
- self.validation_resources['keypair']['private_key'],
+ validation_resources['keypair']['private_key'],
server=server,
servers_client=self.servers_client)
# NOTE(andreaf) We need to ensure the ssh key has been
@@ -111,7 +113,7 @@
@decorators.idempotent_id('7fa563fe-f0f7-43eb-9e22-a1ece036b513')
def test_list_get_volume_attachments(self):
# List volume attachment of the server
- server = self._create_server()
+ server, _ = self._create_server()
volume_1st = self.create_volume()
attachment_1st = self.attach_volume(server, volume_1st,
device=('/dev/%s' % self.device))
@@ -163,15 +165,15 @@
if not CONF.compute_feature_enabled.shelve:
raise cls.skipException('Shelve is not available.')
- def _count_volumes(self, server):
+ def _count_volumes(self, server, validation_resources):
# Count number of volumes on an instance
volumes = 0
if CONF.validation.run_validation:
linux_client = remote_client.RemoteClient(
- self.get_server_ip(server),
+ self.get_server_ip(server, validation_resources),
self.image_ssh_user,
self.image_ssh_password,
- self.validation_resources['keypair']['private_key'],
+ validation_resources['keypair']['private_key'],
server=server,
servers_client=self.servers_client)
@@ -179,7 +181,7 @@
volumes = int(linux_client.exec_command(command).strip())
return volumes
- def _shelve_server(self, server):
+ def _shelve_server(self, server, validation_resources):
# NOTE(andreaf) If we are going to shelve a server, we should
# check first whether the server is ssh-able. Otherwise we
# won't be able to distinguish failures introduced by shelve
@@ -188,10 +190,10 @@
# avoid breaking the VM
if CONF.validation.run_validation:
linux_client = remote_client.RemoteClient(
- self.get_server_ip(server),
+ self.get_server_ip(server, validation_resources),
self.image_ssh_user,
self.image_ssh_password,
- self.validation_resources['keypair']['private_key'],
+ validation_resources['keypair']['private_key'],
server=server,
servers_client=self.servers_client)
linux_client.validate_authentication()
@@ -199,30 +201,33 @@
# If validation went ok, or it was skipped, shelve the server
compute.shelve_server(self.servers_client, server['id'])
- def _unshelve_server_and_check_volumes(self, server, number_of_volumes):
+ def _unshelve_server_and_check_volumes(self, server,
+ validation_resources,
+ number_of_volumes):
# Unshelve the instance and check that there are expected volumes
self.servers_client.unshelve_server(server['id'])
waiters.wait_for_server_status(self.servers_client,
server['id'],
'ACTIVE')
if CONF.validation.run_validation:
- counted_volumes = self._count_volumes(server)
+ counted_volumes = self._count_volumes(
+ server, validation_resources)
self.assertEqual(number_of_volumes, counted_volumes)
@decorators.idempotent_id('13a940b6-3474-4c3c-b03f-29b89112bfee')
def test_attach_volume_shelved_or_offload_server(self):
# Create server, count number of volumes on it, shelve
# server and attach pre-created volume to shelved server
- server = self._create_server()
+ server, validation_resources = self._create_server()
volume = self.create_volume()
- num_vol = self._count_volumes(server)
- self._shelve_server(server)
+ num_vol = self._count_volumes(server, validation_resources)
+ self._shelve_server(server, validation_resources)
attachment = self.attach_volume(server, volume,
- device=('/dev/%s' % self.device),
- check_reserved=True)
+ device=('/dev/%s' % self.device))
# Unshelve the instance and check that attached volume exists
- self._unshelve_server_and_check_volumes(server, num_vol + 1)
+ self._unshelve_server_and_check_volumes(
+ server, validation_resources, num_vol + 1)
# Get volume attachment of the server
volume_attachment = self.servers_client.show_volume_attachment(
@@ -238,18 +243,18 @@
def test_detach_volume_shelved_or_offload_server(self):
# Count number of volumes on instance, shelve
# server and attach pre-created volume to shelved server
- server = self._create_server()
+ server, validation_resources = self._create_server()
volume = self.create_volume()
- num_vol = self._count_volumes(server)
- self._shelve_server(server)
+ num_vol = self._count_volumes(server, validation_resources)
+ self._shelve_server(server, validation_resources)
# Attach and then detach the volume
- self.attach_volume(server, volume, device=('/dev/%s' % self.device),
- check_reserved=True)
+ self.attach_volume(server, volume, device=('/dev/%s' % self.device))
self.servers_client.detach_volume(server['id'], volume['id'])
waiters.wait_for_volume_resource_status(self.volumes_client,
volume['id'], 'available')
# Unshelve the instance and check that we have the expected number of
# volume(s)
- self._unshelve_server_and_check_volumes(server, num_vol)
+ self._unshelve_server_and_check_volumes(
+ server, validation_resources, num_vol)
diff --git a/tempest/api/identity/admin/v2/test_endpoints.py b/tempest/api/identity/admin/v2/test_endpoints.py
index 59fc4d8..947706e 100644
--- a/tempest/api/identity/admin/v2/test_endpoints.py
+++ b/tempest/api/identity/admin/v2/test_endpoints.py
@@ -23,15 +23,15 @@
@classmethod
def resource_setup(cls):
super(EndPointsTestJSON, cls).resource_setup()
- cls.service_ids = list()
s_name = data_utils.rand_name('service')
s_type = data_utils.rand_name('type')
s_description = data_utils.rand_name('description')
service_data = cls.services_client.create_service(
name=s_name, type=s_type,
description=s_description)['OS-KSADM:service']
+ cls.addClassResourceCleanup(cls.services_client.delete_service,
+ service_data['id'])
cls.service_id = service_data['id']
- cls.service_ids.append(cls.service_id)
# Create endpoints so as to use for LIST and GET test cases
cls.setup_endpoints = list()
for _ in range(2):
@@ -43,18 +43,12 @@
publicurl=url,
adminurl=url,
internalurl=url)['endpoint']
+ cls.addClassResourceCleanup(cls.endpoints_client.delete_endpoint,
+ endpoint['id'])
# list_endpoints() will return 'enabled' field
endpoint['enabled'] = True
cls.setup_endpoints.append(endpoint)
- @classmethod
- def resource_cleanup(cls):
- for e in cls.setup_endpoints:
- cls.endpoints_client.delete_endpoint(e['id'])
- for s in cls.service_ids:
- cls.services_client.delete_service(s)
- super(EndPointsTestJSON, cls).resource_cleanup()
-
@decorators.idempotent_id('11f590eb-59d8-4067-8b2b-980c7f387f51')
def test_list_endpoints(self):
# Get a list of endpoints
diff --git a/tempest/api/identity/admin/v2/test_roles.py b/tempest/api/identity/admin/v2/test_roles.py
index 124bb5f..9736a76 100644
--- a/tempest/api/identity/admin/v2/test_roles.py
+++ b/tempest/api/identity/admin/v2/test_roles.py
@@ -28,14 +28,11 @@
for _ in range(5):
role_name = data_utils.rand_name(name='role')
role = cls.roles_client.create_role(name=role_name)['role']
+ cls.addClassResourceCleanup(
+ test_utils.call_and_ignore_notfound_exc,
+ cls.roles_client.delete_role, role['id'])
cls.roles.append(role)
- @classmethod
- def resource_cleanup(cls):
- super(RolesTestJSON, cls).resource_cleanup()
- for role in cls.roles:
- cls.roles_client.delete_role(role['id'])
-
def _get_role_params(self):
user = self.setup_test_user()
tenant = self.tenants_client.show_tenant(user['tenantId'])['tenant']
diff --git a/tempest/api/identity/admin/v3/test_domains.py b/tempest/api/identity/admin/v3/test_domains.py
index bf04ede..ca6b03e 100644
--- a/tempest/api/identity/admin/v3/test_domains.py
+++ b/tempest/api/identity/admin/v3/test_domains.py
@@ -34,19 +34,6 @@
domain = cls.create_domain(enabled=i < 2)
cls.setup_domains.append(domain)
- @classmethod
- def resource_cleanup(cls):
- for domain in cls.setup_domains:
- cls._delete_domain(domain['id'])
- super(DomainsTestJSON, cls).resource_cleanup()
-
- @classmethod
- def _delete_domain(cls, domain_id):
- # It is necessary to disable the domain before deleting,
- # or else it would result in unauthorized error
- cls.domains_client.update_domain(domain_id, enabled=False)
- cls.domains_client.delete_domain(domain_id)
-
@decorators.idempotent_id('8cf516ef-2114-48f1-907b-d32726c734d4')
def test_list_domains(self):
# Test to list domains
@@ -92,7 +79,7 @@
domain = self.domains_client.create_domain(
name=d_name, description=d_desc)['domain']
self.addCleanup(test_utils.call_and_ignore_notfound_exc,
- self._delete_domain, domain['id'])
+ self.delete_domain, domain['id'])
self.assertIn('description', domain)
self.assertIn('name', domain)
self.assertIn('enabled', domain)
@@ -145,7 +132,7 @@
# Create domain only with name
d_name = data_utils.rand_name('domain')
domain = self.domains_client.create_domain(name=d_name)['domain']
- self.addCleanup(self._delete_domain, domain['id'])
+ self.addCleanup(self.delete_domain, domain['id'])
expected_data = {'name': d_name, 'enabled': True}
self.assertEqual('', domain['description'])
self.assertDictContainsSubset(expected_data, domain)
diff --git a/tempest/api/identity/admin/v3/test_endpoints_negative.py b/tempest/api/identity/admin/v3/test_endpoints_negative.py
index 70dd7b5..d54e222 100644
--- a/tempest/api/identity/admin/v3/test_endpoints_negative.py
+++ b/tempest/api/identity/admin/v3/test_endpoints_negative.py
@@ -30,7 +30,6 @@
@classmethod
def resource_setup(cls):
super(EndpointsNegativeTestJSON, cls).resource_setup()
- cls.service_ids = list()
s_name = data_utils.rand_name('service')
s_type = data_utils.rand_name('type')
s_description = data_utils.rand_name('description')
@@ -38,14 +37,10 @@
cls.services_client.create_service(name=s_name, type=s_type,
description=s_description)
['service'])
- cls.service_id = service_data['id']
- cls.service_ids.append(cls.service_id)
+ cls.addClassResourceCleanup(cls.services_client.delete_service,
+ service_data['id'])
- @classmethod
- def resource_cleanup(cls):
- for s in cls.service_ids:
- cls.services_client.delete_service(s)
- super(EndpointsNegativeTestJSON, cls).resource_cleanup()
+ cls.service_id = service_data['id']
@decorators.attr(type=['negative'])
@decorators.idempotent_id('ac6c137e-4d3d-448f-8c83-4f13d0942651')
diff --git a/tempest/api/identity/admin/v3/test_groups.py b/tempest/api/identity/admin/v3/test_groups.py
index 17db3ea..507810b 100644
--- a/tempest/api/identity/admin/v3/test_groups.py
+++ b/tempest/api/identity/admin/v3/test_groups.py
@@ -28,13 +28,6 @@
super(GroupsV3TestJSON, cls).resource_setup()
cls.domain = cls.create_domain()
- @classmethod
- def resource_cleanup(cls):
- # Cleanup the domains created in the setup
- cls.domains_client.update_domain(cls.domain['id'], enabled=False)
- cls.domains_client.delete_domain(cls.domain['id'])
- super(GroupsV3TestJSON, cls).resource_cleanup()
-
@decorators.idempotent_id('2e80343b-6c81-4ac3-88c7-452f3e9d5129')
def test_group_create_update_get(self):
name = data_utils.rand_name('Group')
diff --git a/tempest/api/identity/admin/v3/test_inherits.py b/tempest/api/identity/admin/v3/test_inherits.py
index 8b687cd..c0c79b9 100644
--- a/tempest/api/identity/admin/v3/test_inherits.py
+++ b/tempest/api/identity/admin/v3/test_inherits.py
@@ -49,8 +49,6 @@
cls.groups_client.delete_group(cls.group['id'])
cls.users_client.delete_user(cls.user['id'])
cls.projects_client.delete_project(cls.project['id'])
- cls.domains_client.update_domain(cls.domain['id'], enabled=False)
- cls.domains_client.delete_domain(cls.domain['id'])
super(InheritsV3TestJSON, cls).resource_cleanup()
def _list_assertions(self, body, fetched_role_ids, role_id):
diff --git a/tempest/api/identity/admin/v3/test_list_projects.py b/tempest/api/identity/admin/v3/test_list_projects.py
index 7e70c14..82664e8 100644
--- a/tempest/api/identity/admin/v3/test_list_projects.py
+++ b/tempest/api/identity/admin/v3/test_list_projects.py
@@ -27,35 +27,27 @@
# Create a domain
cls.domain = cls.create_domain()
# Create project with domain
- cls.projects = list()
cls.p1_name = data_utils.rand_name('project')
cls.p1 = cls.projects_client.create_project(
cls.p1_name, enabled=False,
domain_id=cls.domain['id'])['project']
- cls.projects.append(cls.p1)
+ cls.addClassResourceCleanup(cls.projects_client.delete_project,
+ cls.p1['id'])
cls.project_ids.append(cls.p1['id'])
# Create default project
p2_name = data_utils.rand_name('project')
cls.p2 = cls.projects_client.create_project(p2_name)['project']
- cls.projects.append(cls.p2)
+ cls.addClassResourceCleanup(cls.projects_client.delete_project,
+ cls.p2['id'])
cls.project_ids.append(cls.p2['id'])
# Create a new project (p3) using p2 as parent project
p3_name = data_utils.rand_name('project')
cls.p3 = cls.projects_client.create_project(
p3_name, parent_id=cls.p2['id'])['project']
- cls.projects.append(cls.p3)
+ cls.addClassResourceCleanup(cls.projects_client.delete_project,
+ cls.p3['id'])
cls.project_ids.append(cls.p3['id'])
- @classmethod
- def resource_cleanup(cls):
- # Cleanup the projects created during setup in inverse order
- for project in reversed(cls.projects):
- cls.projects_client.delete_project(project['id'])
- # Cleanup the domain created during setup
- cls.domains_client.update_domain(cls.domain['id'], enabled=False)
- cls.domains_client.delete_domain(cls.domain['id'])
- super(ListProjectsTestJSON, cls).resource_cleanup()
-
@decorators.idempotent_id('1d830662-22ad-427c-8c3e-4ec854b0af44')
def test_list_projects(self):
# List projects
diff --git a/tempest/api/identity/admin/v3/test_list_users.py b/tempest/api/identity/admin/v3/test_list_users.py
index 506c729..c69e4c8 100644
--- a/tempest/api/identity/admin/v3/test_list_users.py
+++ b/tempest/api/identity/admin/v3/test_list_users.py
@@ -47,24 +47,18 @@
cls.domain_enabled_user = cls.users_client.create_user(
name=u1_name, password=alt_password,
email=cls.alt_email, domain_id=cls.domain['id'])['user']
+ cls.addClassResourceCleanup(cls.users_client.delete_user,
+ cls.domain_enabled_user['id'])
cls.users.append(cls.domain_enabled_user)
# Create default not enabled user
u2_name = data_utils.rand_name('test_user')
cls.non_domain_enabled_user = cls.users_client.create_user(
name=u2_name, password=alt_password,
email=cls.alt_email, enabled=False)['user']
+ cls.addClassResourceCleanup(cls.users_client.delete_user,
+ cls.non_domain_enabled_user['id'])
cls.users.append(cls.non_domain_enabled_user)
- @classmethod
- def resource_cleanup(cls):
- # Cleanup the users created during setup
- for user in cls.users:
- cls.users_client.delete_user(user['id'])
- # Cleanup the domain created during setup
- cls.domains_client.update_domain(cls.domain['id'], enabled=False)
- cls.domains_client.delete_domain(cls.domain['id'])
- super(UsersV3TestJSON, cls).resource_cleanup()
-
@decorators.idempotent_id('08f9aabb-dcfe-41d0-8172-82b5fa0bd73d')
def test_list_user_domains(self):
# List users with domain
diff --git a/tempest/api/identity/admin/v3/test_oauth_consumers.py b/tempest/api/identity/admin/v3/test_oauth_consumers.py
index 970ead3..062cce5 100644
--- a/tempest/api/identity/admin/v3/test_oauth_consumers.py
+++ b/tempest/api/identity/admin/v3/test_oauth_consumers.py
@@ -17,7 +17,7 @@
from tempest.lib.common.utils import data_utils
from tempest.lib.common.utils import test_utils
from tempest.lib import decorators
-from tempest.lib import exceptions as exceptions
+from tempest.lib import exceptions
class OAUTHConsumersV3Test(base.BaseIdentityV3AdminTest):
diff --git a/tempest/api/identity/admin/v3/test_projects.py b/tempest/api/identity/admin/v3/test_projects.py
index 1b1d3f7..ac23067 100644
--- a/tempest/api/identity/admin/v3/test_projects.py
+++ b/tempest/api/identity/admin/v3/test_projects.py
@@ -87,7 +87,8 @@
# project and domain APIs
projects_list = self.projects_client.list_projects(
params={'is_domain': True})['projects']
- self.assertIn(project, projects_list)
+ project_ids = [p['id'] for p in projects_list]
+ self.assertIn(project['id'], project_ids)
# The domains API return different attributes for the entity, so we
# compare the entities IDs
@@ -205,3 +206,31 @@
self.assertEqual(project['id'],
new_user_get['project_id'])
self.assertEqual(u_email, new_user_get['email'])
+
+ @decorators.idempotent_id('d1db68b6-aebe-4fa0-b79d-d724d2e21162')
+ def test_project_get_equals_list(self):
+ fields = ['parent_id', 'is_domain', 'description', 'links',
+ 'name', 'enabled', 'domain_id', 'id', 'tags']
+
+ # Tags must be unique, keystone API will reject duplicates
+ tags = ['a', 'c', 'b', 'd']
+
+ # Create a Project, cleanup is handled in the helper
+ project = self.setup_test_project(tags=tags)
+
+ # Show and list for the project
+ project_get = self.projects_client.show_project(
+ project['id'])['project']
+ _projects = self.projects_client.list_projects()['projects']
+ project_list = next(x for x in _projects if x['id'] == project['id'])
+
+ # Assert the list of fields is correct (one is enough to check here)
+ self.assertSetEqual(set(fields), set(project_get.keys()))
+
+ # Ensure the set of tags is identical and match the expected one
+ get_tags = set(project_get.pop("tags"))
+ self.assertSetEqual(get_tags, set(project_list.pop("tags")))
+ self.assertSetEqual(get_tags, set(tags))
+
+ # Ensure all other fields are identical
+ self.assertDictEqual(project_get, project_list)
diff --git a/tempest/api/identity/admin/v3/test_regions.py b/tempest/api/identity/admin/v3/test_regions.py
index d00e408..f22a528 100644
--- a/tempest/api/identity/admin/v3/test_regions.py
+++ b/tempest/api/identity/admin/v3/test_regions.py
@@ -34,14 +34,10 @@
r_description = data_utils.rand_name('description')
region = cls.client.create_region(
description=r_description)['region']
+ cls.addClassResourceCleanup(
+ cls.client.delete_region, region['id'])
cls.setup_regions.append(region)
- @classmethod
- def resource_cleanup(cls):
- for r in cls.setup_regions:
- cls.client.delete_region(r['id'])
- super(RegionsTestJSON, cls).resource_cleanup()
-
@decorators.idempotent_id('56186092-82e4-43f2-b954-91013218ba42')
def test_create_update_get_delete_region(self):
# Create region
diff --git a/tempest/api/identity/admin/v3/test_roles.py b/tempest/api/identity/admin/v3/test_roles.py
index ec904e6..7cd41e9 100644
--- a/tempest/api/identity/admin/v3/test_roles.py
+++ b/tempest/api/identity/admin/v3/test_roles.py
@@ -32,6 +32,8 @@
for _ in range(3):
role_name = data_utils.rand_name(name='role')
role = cls.roles_client.create_role(name=role_name)['role']
+ cls.addClassResourceCleanup(cls.roles_client.delete_role,
+ role['id'])
cls.roles.append(role)
u_name = data_utils.rand_name('user')
u_desc = '%s description' % u_name
@@ -42,29 +44,23 @@
data_utils.rand_name('project'),
description=data_utils.rand_name('project-desc'),
domain_id=cls.domain['id'])['project']
+ cls.addClassResourceCleanup(cls.projects_client.delete_project,
+ cls.project['id'])
cls.group_body = cls.groups_client.create_group(
name=data_utils.rand_name('Group'), project_id=cls.project['id'],
domain_id=cls.domain['id'])['group']
+ cls.addClassResourceCleanup(cls.groups_client.delete_group,
+ cls.group_body['id'])
cls.user_body = cls.users_client.create_user(
name=u_name, description=u_desc, password=cls.u_password,
email=u_email, project_id=cls.project['id'],
domain_id=cls.domain['id'])['user']
+ cls.addClassResourceCleanup(cls.users_client.delete_user,
+ cls.user_body['id'])
cls.role = cls.roles_client.create_role(
name=data_utils.rand_name('Role'))['role']
-
- @classmethod
- def resource_cleanup(cls):
- cls.roles_client.delete_role(cls.role['id'])
- cls.groups_client.delete_group(cls.group_body['id'])
- cls.users_client.delete_user(cls.user_body['id'])
- cls.projects_client.delete_project(cls.project['id'])
- # NOTE(harika-vakadi): It is necessary to disable the domain
- # before deleting,or else it would result in unauthorized error
- cls.domains_client.update_domain(cls.domain['id'], enabled=False)
- cls.domains_client.delete_domain(cls.domain['id'])
- for role in cls.roles:
- cls.roles_client.delete_role(role['id'])
- super(RolesV3TestJSON, cls).resource_cleanup()
+ cls.addClassResourceCleanup(cls.roles_client.delete_role,
+ cls.role['id'])
@decorators.attr(type='smoke')
@decorators.idempotent_id('18afc6c0-46cf-4911-824e-9989cc056c3a')
diff --git a/tempest/api/identity/admin/v3/test_tokens.py b/tempest/api/identity/admin/v3/test_tokens.py
index 6343ea8..0845407 100644
--- a/tempest/api/identity/admin/v3/test_tokens.py
+++ b/tempest/api/identity/admin/v3/test_tokens.py
@@ -26,6 +26,8 @@
class TokensV3TestJSON(base.BaseIdentityV3AdminTest):
+ credentials = ['primary', 'admin', 'alt']
+
@decorators.idempotent_id('0f9f5a5f-d5cd-4a86-8a5b-c5ded151f212')
def test_tokens(self):
# Valid user's token is authenticated
@@ -163,12 +165,78 @@
# Get available project scopes
available_projects = self.client.list_auth_projects()['projects']
- # create list to save fetched project's id
+ # Create list to save fetched project IDs
fetched_project_ids = [i['id'] for i in available_projects]
# verifying the project ids in list
missing_project_ids = \
[p for p in assigned_project_ids if p not in fetched_project_ids]
self.assertEmpty(missing_project_ids,
- "Failed to find project_id %s in fetched list" %
+ "Failed to find project_ids %s in fetched list" %
', '.join(missing_project_ids))
+
+ @decorators.idempotent_id('ec5ecb05-af64-4c04-ac86-4d9f6f12f185')
+ def test_get_available_domain_scopes(self):
+ # Test for verifying that listing domain scopes for a user works if
+ # the user has a domain role or belongs to a group that has a domain
+ # role. For this test, admin client is used to add roles to alt user,
+ # which performs API calls, to avoid 401 Unauthorized errors.
+ alt_user_id = self.os_alt.credentials.user_id
+
+ def _create_user_domain_role_for_alt_user():
+ domain_id = self.setup_test_domain()['id']
+ role_id = self.setup_test_role()['id']
+
+ # Create a role association between the user and domain.
+ self.roles_client.create_user_role_on_domain(
+ domain_id, alt_user_id, role_id)
+ self.addCleanup(
+ self.roles_client.delete_role_from_user_on_domain,
+ domain_id, alt_user_id, role_id)
+
+ return domain_id
+
+ def _create_group_domain_role_for_alt_user():
+ domain_id = self.setup_test_domain()['id']
+ role_id = self.setup_test_role()['id']
+
+ # Create a group.
+ group_name = data_utils.rand_name('Group')
+ group_id = self.groups_client.create_group(
+ name=group_name, domain_id=domain_id)['group']['id']
+ self.addCleanup(self.groups_client.delete_group, group_id)
+
+ # Add the alt user to the group.
+ self.groups_client.add_group_user(group_id, alt_user_id)
+ self.addCleanup(self.groups_client.delete_group_user,
+ group_id, alt_user_id)
+
+ # Create a role association between the group and domain.
+ self.roles_client.create_group_role_on_domain(
+ domain_id, group_id, role_id)
+ self.addCleanup(
+ self.roles_client.delete_role_from_group_on_domain,
+ domain_id, group_id, role_id)
+
+ return domain_id
+
+ # Add the alt user to 2 random domains and 2 random groups
+ # with randomized domains and roles.
+ assigned_domain_ids = []
+ for _ in range(2):
+ domain_id = _create_user_domain_role_for_alt_user()
+ assigned_domain_ids.append(domain_id)
+ domain_id = _create_group_domain_role_for_alt_user()
+ assigned_domain_ids.append(domain_id)
+
+ # Get available domain scopes for the alt user.
+ available_domains = self.os_alt.identity_v3_client.list_auth_domains()[
+ 'domains']
+ fetched_domain_ids = [i['id'] for i in available_domains]
+
+ # Verify the expected domain IDs are in the list.
+ missing_domain_ids = \
+ [p for p in assigned_domain_ids if p not in fetched_domain_ids]
+ self.assertEmpty(missing_domain_ids,
+ "Failed to find domain_ids %s in fetched list"
+ % ", ".join(missing_domain_ids))
diff --git a/tempest/api/identity/base.py b/tempest/api/identity/base.py
index 30d2a36..9edccbb 100644
--- a/tempest/api/identity/base.py
+++ b/tempest/api/identity/base.py
@@ -249,13 +249,16 @@
if 'description' not in kwargs:
kwargs['description'] = data_utils.rand_name('desc')
domain = cls.domains_client.create_domain(**kwargs)['domain']
+ cls.addClassResourceCleanup(test_utils.call_and_ignore_notfound_exc,
+ cls.delete_domain, domain['id'])
return domain
- def delete_domain(self, domain_id):
+ @classmethod
+ def delete_domain(cls, domain_id):
# NOTE(mpavlase) It is necessary to disable the domain before deleting
# otherwise it raises Forbidden exception
- self.domains_client.update_domain(domain_id, enabled=False)
- self.domains_client.delete_domain(domain_id)
+ cls.domains_client.update_domain(domain_id, enabled=False)
+ cls.domains_client.delete_domain(domain_id)
def setup_test_user(self, password=None):
"""Set up a test user."""
diff --git a/tempest/api/identity/v3/test_catalog.py b/tempest/api/identity/v3/test_catalog.py
old mode 100755
new mode 100644
diff --git a/tempest/api/image/base.py b/tempest/api/image/base.py
index 70ba2fe..7103d56 100644
--- a/tempest/api/image/base.py
+++ b/tempest/api/image/base.py
@@ -46,16 +46,6 @@
cls.created_images = []
@classmethod
- def resource_cleanup(cls):
- for image_id in cls.created_images:
- test_utils.call_and_ignore_notfound_exc(
- cls.client.delete_image, image_id)
-
- for image_id in cls.created_images:
- cls.client.wait_for_resource_deletion(image_id)
- super(BaseImageTest, cls).resource_cleanup()
-
- @classmethod
def create_image(cls, data=None, **kwargs):
"""Wrapper that returns a test image."""
@@ -75,6 +65,10 @@
if 'image' in image:
image = image['image']
cls.created_images.append(image['id'])
+ cls.addClassResourceCleanup(cls.client.wait_for_resource_deletion,
+ image['id'])
+ cls.addClassResourceCleanup(test_utils.call_and_ignore_notfound_exc,
+ cls.client.delete_image, image['id'])
return image
@classmethod
diff --git a/tempest/api/network/admin/test_metering_extensions.py b/tempest/api/network/admin/test_metering_extensions.py
index fd86782..5063fef 100644
--- a/tempest/api/network/admin/test_metering_extensions.py
+++ b/tempest/api/network/admin/test_metering_extensions.py
@@ -15,6 +15,7 @@
from tempest.api.network import base
from tempest.common import utils
from tempest.lib.common.utils import data_utils
+from tempest.lib.common.utils import test_utils
from tempest.lib import decorators
@@ -52,7 +53,10 @@
description=description,
name=name)
metering_label = body['metering_label']
- cls.metering_labels.append(metering_label)
+ cls.addClassResourceCleanup(
+ test_utils.call_and_ignore_notfound_exc,
+ cls.admin_metering_labels_client.delete_metering_label,
+ metering_label['id'])
return metering_label
@classmethod
@@ -64,7 +68,9 @@
remote_ip_prefix=remote_ip_prefix, direction=direction,
metering_label_id=metering_label_id)
metering_label_rule = body['metering_label_rule']
- cls.metering_label_rules.append(metering_label_rule)
+ cls.addClassResourceCleanup(
+ test_utils.call_and_ignore_notfound_exc,
+ client.delete_metering_label_rule, metering_label_rule['id'])
return metering_label_rule
def _delete_metering_label(self, metering_label_id):
diff --git a/tempest/api/network/admin/test_quotas.py b/tempest/api/network/admin/test_quotas.py
index f69d7c5..cf4236d 100644
--- a/tempest/api/network/admin/test_quotas.py
+++ b/tempest/api/network/admin/test_quotas.py
@@ -14,6 +14,7 @@
# under the License.
from tempest.api.network import base
+from tempest.common import identity
from tempest.common import utils
from tempest.lib.common.utils import data_utils
from tempest.lib.common.utils import test_utils
@@ -46,10 +47,11 @@
# Add a project to conduct the test
project = data_utils.rand_name('test_project_')
description = data_utils.rand_name('desc_')
- project = self.identity_utils.create_project(name=project,
- description=description)
+ project = identity.identity_utils(self.os_admin).create_project(
+ name=project, description=description)
project_id = project['id']
- self.addCleanup(self.identity_utils.delete_project, project_id)
+ self.addCleanup(identity.identity_utils(self.os_admin).delete_project,
+ project_id)
# Change quotas for project
quota_set = self.admin_quotas_client.update_quotas(
diff --git a/tempest/api/network/admin/test_routers.py b/tempest/api/network/admin/test_routers.py
index c97efe9..8cdb41e 100644
--- a/tempest/api/network/admin/test_routers.py
+++ b/tempest/api/network/admin/test_routers.py
@@ -16,6 +16,7 @@
import testtools
from tempest.api.network import base
+from tempest.common import identity
from tempest.common import utils
from tempest import config
from tempest.lib.common.utils import data_utils
@@ -41,19 +42,6 @@
self.addCleanup(self._cleanup_router, router)
return router
- def _add_router_interface_with_subnet_id(self, router_id, subnet_id):
- interface = self.routers_client.add_router_interface(
- router_id, subnet_id=subnet_id)
- self.addCleanup(self._remove_router_interface_with_subnet_id,
- router_id, subnet_id)
- self.assertEqual(subnet_id, interface['subnet_id'])
- return interface
-
- def _remove_router_interface_with_subnet_id(self, router_id, subnet_id):
- body = self.routers_client.remove_router_interface(router_id,
- subnet_id=subnet_id)
- self.assertEqual(subnet_id, body['subnet_id'])
-
@classmethod
def skip_checks(cls):
super(RoutersAdminTest, cls).skip_checks()
@@ -66,10 +54,11 @@
# Test creating router from admin user setting project_id.
project = data_utils.rand_name('test_tenant_')
description = data_utils.rand_name('desc_')
- project = self.identity_utils.create_project(name=project,
- description=description)
+ project = identity.identity_utils(self.os_admin).create_project(
+ name=project, description=description)
project_id = project['id']
- self.addCleanup(self.identity_utils.delete_project, project_id)
+ self.addCleanup(identity.identity_utils(self.os_admin).delete_project,
+ project_id)
name = data_utils.rand_name('router-')
create_body = self.admin_routers_client.create_router(
diff --git a/tempest/api/network/base.py b/tempest/api/network/base.py
index 8308e34..bdfda0a 100644
--- a/tempest/api/network/base.py
+++ b/tempest/api/network/base.py
@@ -92,9 +92,6 @@
cls.subnets = []
cls.ports = []
cls.routers = []
- cls.floating_ips = []
- cls.metering_labels = []
- cls.metering_label_rules = []
cls.ethertype = "IPv" + str(cls._ip_version)
if cls._ip_version == 4:
cls.cidr = netaddr.IPNetwork(CONF.network.project_network_cidr)
@@ -106,25 +103,6 @@
@classmethod
def resource_cleanup(cls):
if CONF.service_available.neutron:
- # Clean up floating IPs
- for floating_ip in cls.floating_ips:
- test_utils.call_and_ignore_notfound_exc(
- cls.floating_ips_client.delete_floatingip,
- floating_ip['id'])
-
- # Clean up metering label rules
- # Not all classes in the hierarchy have the client class variable
- if cls.metering_label_rules:
- label_rules_client = cls.admin_metering_label_rules_client
- for metering_label_rule in cls.metering_label_rules:
- test_utils.call_and_ignore_notfound_exc(
- label_rules_client.delete_metering_label_rule,
- metering_label_rule['id'])
- # Clean up metering labels
- for metering_label in cls.metering_labels:
- test_utils.call_and_ignore_notfound_exc(
- cls.admin_metering_labels_client.delete_metering_label,
- metering_label['id'])
# Clean up ports
for port in cls.ports:
test_utils.call_and_ignore_notfound_exc(
@@ -238,7 +216,9 @@
body = cls.floating_ips_client.create_floatingip(
floating_network_id=external_network_id)
fip = body['floatingip']
- cls.floating_ips.append(fip)
+ cls.addClassResourceCleanup(test_utils.call_and_ignore_notfound_exc,
+ cls.floating_ips_client.delete_floatingip,
+ fip['id'])
return fip
@classmethod
diff --git a/tempest/api/network/test_allowed_address_pair.py b/tempest/api/network/test_allowed_address_pair.py
index a471bd6..3075047 100644
--- a/tempest/api/network/test_allowed_address_pair.py
+++ b/tempest/api/network/test_allowed_address_pair.py
@@ -13,7 +13,6 @@
# License for the specific language governing permissions and limitations
# under the License.
-import netaddr
import six
from tempest.api.network import base
@@ -41,8 +40,6 @@
api_extensions
"""
- _project_network_cidr = CONF.network.project_network_cidr
-
@classmethod
def skip_checks(cls):
super(AllowedAddressPairTestJSON, cls).skip_checks()
@@ -105,8 +102,7 @@
@decorators.idempotent_id('4d6d178f-34f6-4bff-a01c-0a2f8fe909e4')
def test_update_port_with_cidr_address_pair(self):
# Update allowed address pair with cidr
- cidr = str(netaddr.IPNetwork(self._project_network_cidr))
- self._update_port_with_address(cidr)
+ self._update_port_with_address(str(self.cidr))
@decorators.idempotent_id('b3f20091-6cd5-472b-8487-3516137df933')
def test_update_port_with_multiple_ip_mac_address_pair(self):
@@ -135,4 +131,3 @@
class AllowedAddressPairIpV6TestJSON(AllowedAddressPairTestJSON):
_ip_version = 6
- _project_network_cidr = CONF.network.project_network_v6_cidr
diff --git a/tempest/api/network/test_routers.py b/tempest/api/network/test_routers.py
index ac315c4..abbb779 100644
--- a/tempest/api/network/test_routers.py
+++ b/tempest/api/network/test_routers.py
@@ -65,9 +65,12 @@
'The public_network_id option must be specified.')
def test_create_show_list_update_delete_router(self):
# Create a router
+ name = data_utils.rand_name(self.__class__.__name__ + '-router')
router = self._create_router(
+ name=name,
admin_state_up=False,
external_network_id=CONF.network.public_network_id)
+ self.assertEqual(router['name'], name)
self.assertEqual(router['admin_state_up'], False)
self.assertEqual(
router['external_gateway_info']['network_id'],
@@ -168,7 +171,7 @@
def test_update_delete_extra_route(self):
# Create different cidr for each subnet to avoid cidr duplicate
# The cidr starts from project_cidr
- next_cidr = netaddr.IPNetwork(self.cidr)
+ next_cidr = self.cidr
# Prepare to build several routes
test_routes = []
routes_num = 4
@@ -244,7 +247,7 @@
network02 = self.create_network(
network_name=data_utils.rand_name('router-network02-'))
subnet01 = self.create_subnet(network01)
- sub02_cidr = netaddr.IPNetwork(self.cidr).next()
+ sub02_cidr = self.cidr.next()
subnet02 = self.create_subnet(network02, cidr=sub02_cidr)
router = self._create_router()
interface01 = self._add_router_interface_with_subnet_id(router['id'],
diff --git a/tempest/api/network/test_routers_negative.py b/tempest/api/network/test_routers_negative.py
index 27bda3a..c9ce55c 100644
--- a/tempest/api/network/test_routers_negative.py
+++ b/tempest/api/network/test_routers_negative.py
@@ -13,8 +13,6 @@
# License for the specific language governing permissions and limitations
# under the License.
-import netaddr
-
from tempest.api.network import base
from tempest.common import utils
from tempest import config
@@ -54,7 +52,7 @@
@decorators.idempotent_id('11836a18-0b15-4327-a50b-f0d9dc66bddd')
def test_router_add_gateway_net_not_external_returns_400(self):
alt_network = self.create_network()
- sub_cidr = netaddr.IPNetwork(self.cidr).next()
+ sub_cidr = self.cidr.next()
self.create_subnet(alt_network, cidr=sub_cidr)
self.assertRaises(lib_exc.BadRequest,
self.routers_client.update_router,
diff --git a/tempest/api/network/test_security_groups.py b/tempest/api/network/test_security_groups.py
index 97ccee9..24bd8ea 100644
--- a/tempest/api/network/test_security_groups.py
+++ b/tempest/api/network/test_security_groups.py
@@ -23,7 +23,6 @@
class SecGroupTest(base.BaseSecGroupTest):
- _project_network_cidr = CONF.network.project_network_cidr
@classmethod
def skip_checks(cls):
@@ -209,7 +208,7 @@
protocol = 'tcp'
port_range_min = 76
port_range_max = 77
- ip_prefix = self._project_network_cidr
+ ip_prefix = str(self.cidr)
self._create_verify_security_group_rule(sg_id, direction,
self.ethertype, protocol,
port_range_min,
@@ -238,4 +237,3 @@
class SecGroupIPv6Test(SecGroupTest):
_ip_version = 6
- _project_network_cidr = CONF.network.project_network_v6_cidr
diff --git a/tempest/api/network/test_security_groups_negative.py b/tempest/api/network/test_security_groups_negative.py
index 435673b..d054865 100644
--- a/tempest/api/network/test_security_groups_negative.py
+++ b/tempest/api/network/test_security_groups_negative.py
@@ -24,7 +24,6 @@
class NegativeSecGroupTest(base.BaseSecGroupTest):
- _project_network_cidr = CONF.network.project_network_cidr
@classmethod
def skip_checks(cls):
@@ -110,7 +109,7 @@
sg2_body, _ = self._create_security_group()
# Create rule specifying both remote_ip_prefix and remote_group_id
- prefix = self._project_network_cidr
+ prefix = str(self.cidr)
self.assertRaises(
lib_exc.BadRequest,
self.security_group_rules_client.create_security_group_rule,
@@ -225,7 +224,6 @@
class NegativeSecGroupIPv6Test(NegativeSecGroupTest):
_ip_version = 6
- _project_network_cidr = CONF.network.project_network_v6_cidr
@decorators.attr(type=['negative'])
@decorators.idempotent_id('7607439c-af73-499e-bf64-f687fd12a842')
diff --git a/tempest/api/network/test_tags.py b/tempest/api/network/test_tags.py
index 409d556..85f6896 100644
--- a/tempest/api/network/test_tags.py
+++ b/tempest/api/network/test_tags.py
@@ -131,11 +131,8 @@
prefix = CONF.network.default_network
cls.subnetpool = cls.subnetpools_client.create_subnetpool(
name=subnetpool_name, prefixes=prefix)['subnetpool']
-
- @classmethod
- def resource_cleanup(cls):
- cls.subnetpools_client.delete_subnetpool(cls.subnetpool['id'])
- super(TagsExtTest, cls).resource_cleanup()
+ cls.addClassResourceCleanup(cls.subnetpools_client.delete_subnetpool,
+ cls.subnetpool['id'])
def _create_tags_for_each_resource(self):
# Create a tag for each resource in `SUPPORTED_RESOURCES` and return
diff --git a/tempest/api/object_storage/base.py b/tempest/api/object_storage/base.py
index 4c49b2a..ee72163 100644
--- a/tempest/api/object_storage/base.py
+++ b/tempest/api/object_storage/base.py
@@ -43,7 +43,7 @@
for cont in containers:
try:
params = {'limit': 9999, 'format': 'json'}
- _, objlist = container_client.list_container_contents(cont, params)
+ _, objlist = container_client.list_container_objects(cont, params)
# delete every object in the container
for obj in objlist:
test_utils.call_and_ignore_notfound_exc(
@@ -106,7 +106,7 @@
def create_container(cls):
# wrapper that returns a test container
container_name = data_utils.rand_name(name='TestContainer')
- cls.container_client.create_container(container_name)
+ cls.container_client.update_container(container_name)
cls.containers.append(container_name)
return container_name
diff --git a/tempest/api/object_storage/test_account_bulk.py b/tempest/api/object_storage/test_account_bulk.py
index 9abd59e..6599e43 100644
--- a/tempest/api/object_storage/test_account_bulk.py
+++ b/tempest/api/object_storage/test_account_bulk.py
@@ -96,7 +96,7 @@
self.assertIn(container_name, [b['name'] for b in body])
param = {'format': 'json'}
- resp, contents_list = self.container_client.list_container_contents(
+ resp, contents_list = self.container_client.list_container_objects(
container_name, param)
self.assertHeaders(resp, 'Container', 'GET')
diff --git a/tempest/api/object_storage/test_account_services.py b/tempest/api/object_storage/test_account_services.py
index 0f86540..d7c85a2 100644
--- a/tempest/api/object_storage/test_account_services.py
+++ b/tempest/api/object_storage/test_account_services.py
@@ -43,7 +43,7 @@
super(AccountTest, cls).resource_setup()
for i in range(ord('a'), ord('f') + 1):
name = data_utils.rand_name(name='%s-' % six.int2byte(i))
- cls.container_client.create_container(name)
+ cls.container_client.update_container(name)
cls.containers.append(name)
cls.containers_count = len(cls.containers)
diff --git a/tempest/api/object_storage/test_container_acl.py b/tempest/api/object_storage/test_container_acl.py
index 4b66ebf..765bc6d 100644
--- a/tempest/api/object_storage/test_container_acl.py
+++ b/tempest/api/object_storage/test_container_acl.py
@@ -41,10 +41,11 @@
tenant_name = self.os_roles_operator_alt.credentials.tenant_name
username = self.os_roles_operator_alt.credentials.username
cont_headers = {'X-Container-Read': tenant_name + ':' + username}
+ container_client = self.os_roles_operator.container_client
resp_meta, _ = (
- self.os_roles_operator.container_client.update_container_metadata(
- self.container_name, metadata=cont_headers,
- metadata_prefix=''))
+ container_client.create_update_or_delete_container_metadata(
+ self.container_name, create_update_metadata=cont_headers,
+ create_update_metadata_prefix=''))
self.assertHeaders(resp_meta, 'Container', 'POST')
# create object
object_name = data_utils.rand_name(name='Object')
@@ -68,10 +69,11 @@
tenant_name = self.os_roles_operator_alt.credentials.tenant_name
username = self.os_roles_operator_alt.credentials.username
cont_headers = {'X-Container-Write': tenant_name + ':' + username}
+ container_client = self.os_roles_operator.container_client
resp_meta, _ = (
- self.os_roles_operator.container_client.update_container_metadata(
- self.container_name, metadata=cont_headers,
- metadata_prefix=''))
+ container_client.create_update_or_delete_container_metadata(
+ self.container_name, create_update_metadata=cont_headers,
+ create_update_metadata_prefix=''))
self.assertHeaders(resp_meta, 'Container', 'POST')
# set alternative authentication data; cannot simply use the
# other object client.
diff --git a/tempest/api/object_storage/test_container_acl_negative.py b/tempest/api/object_storage/test_container_acl_negative.py
index e064753..90b24b4 100644
--- a/tempest/api/object_storage/test_container_acl_negative.py
+++ b/tempest/api/object_storage/test_container_acl_negative.py
@@ -39,7 +39,7 @@
def setUp(self):
super(ObjectACLsNegativeTest, self).setUp()
self.container_name = data_utils.rand_name(name='TestContainer')
- self.container_client.create_container(self.container_name)
+ self.container_client.update_container(self.container_name)
def tearDown(self):
self.delete_containers([self.container_name])
@@ -133,9 +133,10 @@
# attempt to read object using non-authorized user
# update X-Container-Read metadata ACL
cont_headers = {'X-Container-Read': 'badtenant:baduser'}
- resp_meta, _ = self.container_client.update_container_metadata(
- self.container_name, metadata=cont_headers,
- metadata_prefix='')
+ resp_meta, _ = (
+ self.container_client.create_update_or_delete_container_metadata(
+ self.container_name, create_update_metadata=cont_headers,
+ create_update_metadata_prefix=''))
self.assertHeaders(resp_meta, 'Container', 'POST')
# create object
object_name = data_utils.rand_name(name='Object')
@@ -157,9 +158,10 @@
# attempt to write object using non-authorized user
# update X-Container-Write metadata ACL
cont_headers = {'X-Container-Write': 'badtenant:baduser'}
- resp_meta, _ = self.container_client.update_container_metadata(
- self.container_name, metadata=cont_headers,
- metadata_prefix='')
+ resp_meta, _ = (
+ self.container_client.create_update_or_delete_container_metadata(
+ self.container_name, create_update_metadata=cont_headers,
+ create_update_metadata_prefix=''))
self.assertHeaders(resp_meta, 'Container', 'POST')
# Trying to write the object without rights
self.object_client.auth_provider.set_alt_auth_data(
@@ -182,9 +184,10 @@
cont_headers = {'X-Container-Read':
tenant_name + ':' + username,
'X-Container-Write': ''}
- resp_meta, _ = self.container_client.update_container_metadata(
- self.container_name, metadata=cont_headers,
- metadata_prefix='')
+ resp_meta, _ = (
+ self.container_client.create_update_or_delete_container_metadata(
+ self.container_name, create_update_metadata=cont_headers,
+ create_update_metadata_prefix=''))
self.assertHeaders(resp_meta, 'Container', 'POST')
# Trying to write the object without write rights
self.object_client.auth_provider.set_alt_auth_data(
@@ -207,9 +210,10 @@
cont_headers = {'X-Container-Read':
tenant_name + ':' + username,
'X-Container-Write': ''}
- resp_meta, _ = self.container_client.update_container_metadata(
- self.container_name, metadata=cont_headers,
- metadata_prefix='')
+ resp_meta, _ = (
+ self.container_client.create_update_or_delete_container_metadata(
+ self.container_name, create_update_metadata=cont_headers,
+ create_update_metadata_prefix=''))
self.assertHeaders(resp_meta, 'Container', 'POST')
# create object
object_name = data_utils.rand_name(name='Object')
diff --git a/tempest/api/object_storage/test_container_quotas.py b/tempest/api/object_storage/test_container_quotas.py
index c87bed5..982c4a1 100644
--- a/tempest/api/object_storage/test_container_quotas.py
+++ b/tempest/api/object_storage/test_container_quotas.py
@@ -40,8 +40,8 @@
self.container_name = self.create_container()
metadata = {"quota-bytes": str(QUOTA_BYTES),
"quota-count": str(QUOTA_COUNT), }
- self.container_client.update_container_metadata(
- self.container_name, metadata)
+ self.container_client.create_update_or_delete_container_metadata(
+ self.container_name, create_update_metadata=metadata)
def tearDown(self):
"""Cleans the container of any object after each test."""
diff --git a/tempest/api/object_storage/test_container_services.py b/tempest/api/object_storage/test_container_services.py
index 76fe8d4..cdc420e 100644
--- a/tempest/api/object_storage/test_container_services.py
+++ b/tempest/api/object_storage/test_container_services.py
@@ -27,7 +27,7 @@
@decorators.idempotent_id('92139d73-7819-4db1-85f8-3f2f22a8d91f')
def test_create_container(self):
container_name = data_utils.rand_name(name='TestContainer')
- resp, _ = self.container_client.create_container(container_name)
+ resp, _ = self.container_client.update_container(container_name)
self.containers.append(container_name)
self.assertHeaders(resp, 'Container', 'PUT')
@@ -35,20 +35,20 @@
def test_create_container_overwrite(self):
# overwrite container with the same name
container_name = data_utils.rand_name(name='TestContainer')
- self.container_client.create_container(container_name)
+ self.container_client.update_container(container_name)
self.containers.append(container_name)
- resp, _ = self.container_client.create_container(container_name)
+ resp, _ = self.container_client.update_container(container_name)
self.assertHeaders(resp, 'Container', 'PUT')
@decorators.idempotent_id('c2ac4d59-d0f5-40d5-ba19-0635056d48cd')
def test_create_container_with_metadata_key(self):
# create container with the blank value of metadata
container_name = data_utils.rand_name(name='TestContainer')
- metadata = {'test-container-meta': ''}
- resp, _ = self.container_client.create_container(
+ headers = {'X-Container-Meta-test-container-meta': ''}
+ resp, _ = self.container_client.update_container(
container_name,
- metadata=metadata)
+ **headers)
self.containers.append(container_name)
self.assertHeaders(resp, 'Container', 'PUT')
@@ -64,10 +64,10 @@
container_name = data_utils.rand_name(name='TestContainer')
# metadata name using underscores should be converted to hyphens
- metadata = {'test_container_meta': 'Meta1'}
- resp, _ = self.container_client.create_container(
+ headers = {'X-Container-Meta-test_container_meta': 'Meta1'}
+ resp, _ = self.container_client.update_container(
container_name,
- metadata=metadata)
+ **headers)
self.containers.append(container_name)
self.assertHeaders(resp, 'Container', 'PUT')
@@ -75,22 +75,20 @@
container_name)
self.assertIn('x-container-meta-test-container-meta', resp)
self.assertEqual(resp['x-container-meta-test-container-meta'],
- metadata['test_container_meta'])
+ headers['X-Container-Meta-test_container_meta'])
@decorators.idempotent_id('24d16451-1c0c-4e4f-b59c-9840a3aba40e')
def test_create_container_with_remove_metadata_key(self):
# create container with the blank value of remove metadata
container_name = data_utils.rand_name(name='TestContainer')
- metadata_1 = {'test-container-meta': 'Meta1'}
- self.container_client.create_container(
- container_name,
- metadata=metadata_1)
+ headers = {'X-Container-Meta-test-container-meta': 'Meta1'}
+ self.container_client.update_container(container_name, **headers)
self.containers.append(container_name)
- metadata_2 = {'test-container-meta': ''}
- resp, _ = self.container_client.create_container(
+ headers = {'X-Remove-Container-Meta-test-container-meta': ''}
+ resp, _ = self.container_client.update_container(
container_name,
- remove_metadata=metadata_2)
+ **headers)
self.assertHeaders(resp, 'Container', 'PUT')
resp, _ = self.container_client.list_container_metadata(
@@ -101,14 +99,13 @@
def test_create_container_with_remove_metadata_value(self):
# create container with remove metadata
container_name = data_utils.rand_name(name='TestContainer')
- metadata = {'test-container-meta': 'Meta1'}
- self.container_client.create_container(container_name,
- metadata=metadata)
+ headers = {'X-Container-Meta-test-container-meta': 'Meta1'}
+ self.container_client.update_container(container_name, **headers)
self.containers.append(container_name)
-
- resp, _ = self.container_client.create_container(
+ headers = {'X-Remove-Container-Meta-test-container-meta': 'Meta1'}
+ resp, _ = self.container_client.update_container(
container_name,
- remove_metadata=metadata)
+ **headers)
self.assertHeaders(resp, 'Container', 'PUT')
resp, _ = self.container_client.list_container_metadata(
@@ -130,7 +127,7 @@
container_name = self.create_container()
object_name, _ = self.create_object(container_name)
- resp, object_list = self.container_client.list_container_contents(
+ resp, object_list = self.container_client.list_container_objects(
container_name)
self.assertHeaders(resp, 'Container', 'GET')
self.assertEqual([object_name], object_list)
@@ -140,7 +137,7 @@
# get empty container contents list
container_name = self.create_container()
- resp, object_list = self.container_client.list_container_contents(
+ resp, object_list = self.container_client.list_container_objects(
container_name)
self.assertHeaders(resp, 'Container', 'GET')
self.assertEmpty(object_list)
@@ -153,7 +150,7 @@
self.create_object(container_name, object_name)
params = {'delimiter': '/'}
- resp, object_list = self.container_client.list_container_contents(
+ resp, object_list = self.container_client.list_container_objects(
container_name,
params=params)
self.assertHeaders(resp, 'Container', 'GET')
@@ -166,7 +163,7 @@
object_name, _ = self.create_object(container_name)
params = {'end_marker': object_name + 'zzzz'}
- resp, object_list = self.container_client.list_container_contents(
+ resp, object_list = self.container_client.list_container_objects(
container_name,
params=params)
self.assertHeaders(resp, 'Container', 'GET')
@@ -179,7 +176,7 @@
self.create_object(container_name)
params = {'format': 'json'}
- resp, object_list = self.container_client.list_container_contents(
+ resp, object_list = self.container_client.list_container_objects(
container_name,
params=params)
self.assertHeaders(resp, 'Container', 'GET')
@@ -198,7 +195,7 @@
self.create_object(container_name)
params = {'format': 'xml'}
- resp, object_list = self.container_client.list_container_contents(
+ resp, object_list = self.container_client.list_container_objects(
container_name,
params=params)
self.assertHeaders(resp, 'Container', 'GET')
@@ -222,7 +219,7 @@
object_name, _ = self.create_object(container_name)
params = {'limit': data_utils.rand_int_id(1, 10000)}
- resp, object_list = self.container_client.list_container_contents(
+ resp, object_list = self.container_client.list_container_objects(
container_name,
params=params)
self.assertHeaders(resp, 'Container', 'GET')
@@ -235,7 +232,7 @@
object_name, _ = self.create_object(container_name)
params = {'marker': 'AaaaObject1234567890'}
- resp, object_list = self.container_client.list_container_contents(
+ resp, object_list = self.container_client.list_container_objects(
container_name,
params=params)
self.assertHeaders(resp, 'Container', 'GET')
@@ -250,7 +247,7 @@
self.create_object(container_name, object_name)
params = {'path': 'Swift'}
- resp, object_list = self.container_client.list_container_contents(
+ resp, object_list = self.container_client.list_container_objects(
container_name,
params=params)
self.assertHeaders(resp, 'Container', 'GET')
@@ -264,7 +261,7 @@
prefix_key = object_name[0:8]
params = {'prefix': prefix_key}
- resp, object_list = self.container_client.list_container_contents(
+ resp, object_list = self.container_client.list_container_objects(
container_name,
params=params)
self.assertHeaders(resp, 'Container', 'GET')
@@ -277,9 +274,9 @@
container_name = self.create_container()
metadata = {'name': 'Pictures'}
- self.container_client.update_container_metadata(
+ self.container_client.create_update_or_delete_container_metadata(
container_name,
- metadata=metadata)
+ create_update_metadata=metadata)
resp, _ = self.container_client.list_container_metadata(
container_name)
@@ -301,16 +298,16 @@
def test_update_container_metadata_with_create_and_delete_metadata(self):
# Send one request of adding and deleting metadata
container_name = data_utils.rand_name(name='TestContainer')
- metadata_1 = {'test-container-meta1': 'Meta1'}
- self.container_client.create_container(container_name,
- metadata=metadata_1)
+ metadata_1 = {'X-Container-Meta-test-container-meta1': 'Meta1'}
+ self.container_client.update_container(container_name, **metadata_1)
self.containers.append(container_name)
metadata_2 = {'test-container-meta2': 'Meta2'}
- resp, _ = self.container_client.update_container_metadata(
- container_name,
- metadata=metadata_2,
- remove_metadata=metadata_1)
+ resp, _ = (
+ self.container_client.create_update_or_delete_container_metadata(
+ container_name,
+ create_update_metadata=metadata_2,
+ delete_metadata={'test-container-meta1': 'Meta1'}))
self.assertHeaders(resp, 'Container', 'POST')
resp, _ = self.container_client.list_container_metadata(
@@ -326,9 +323,10 @@
container_name = self.create_container()
metadata = {'test-container-meta1': 'Meta1'}
- resp, _ = self.container_client.update_container_metadata(
- container_name,
- metadata=metadata)
+ resp, _ = (
+ self.container_client.create_update_or_delete_container_metadata(
+ container_name,
+ create_update_metadata=metadata))
self.assertHeaders(resp, 'Container', 'POST')
resp, _ = self.container_client.list_container_metadata(
@@ -341,14 +339,14 @@
def test_update_container_metadata_with_delete_metadata(self):
# update container metadata using delete metadata
container_name = data_utils.rand_name(name='TestContainer')
- metadata = {'test-container-meta1': 'Meta1'}
- self.container_client.create_container(container_name,
- metadata=metadata)
+ metadata = {'X-Container-Meta-test-container-meta1': 'Meta1'}
+ self.container_client.update_container(container_name, **metadata)
self.containers.append(container_name)
- resp, _ = self.container_client.delete_container_metadata(
- container_name,
- metadata=metadata)
+ resp, _ = (
+ self.container_client.create_update_or_delete_container_metadata(
+ container_name,
+ delete_metadata={'test-container-meta1': 'Meta1'}))
self.assertHeaders(resp, 'Container', 'POST')
resp, _ = self.container_client.list_container_metadata(
@@ -361,9 +359,10 @@
container_name = self.create_container()
metadata = {'test-container-meta1': ''}
- resp, _ = self.container_client.update_container_metadata(
- container_name,
- metadata=metadata)
+ resp, _ = (
+ self.container_client.create_update_or_delete_container_metadata(
+ container_name,
+ create_update_metadata=metadata))
self.assertHeaders(resp, 'Container', 'POST')
resp, _ = self.container_client.list_container_metadata(
@@ -374,15 +373,15 @@
def test_update_container_metadata_with_delete_metadata_key(self):
# update container metadata with a blank value of metadata
container_name = data_utils.rand_name(name='TestContainer')
- metadata = {'test-container-meta1': 'Meta1'}
- self.container_client.create_container(container_name,
- metadata=metadata)
+ headers = {'X-Container-Meta-test-container-meta1': 'Meta1'}
+ self.container_client.update_container(container_name, **headers)
self.containers.append(container_name)
metadata = {'test-container-meta1': ''}
- resp, _ = self.container_client.delete_container_metadata(
- container_name,
- metadata=metadata)
+ resp, _ = (
+ self.container_client.create_update_or_delete_container_metadata(
+ container_name,
+ delete_metadata=metadata))
self.assertHeaders(resp, 'Container', 'POST')
resp, _ = self.container_client.list_container_metadata(container_name)
diff --git a/tempest/api/object_storage/test_container_services_negative.py b/tempest/api/object_storage/test_container_services_negative.py
index 387b7b6..b8c83b7 100644
--- a/tempest/api/object_storage/test_container_services_negative.py
+++ b/tempest/api/object_storage/test_container_services_negative.py
@@ -45,9 +45,10 @@
max_length = self.constraints['max_container_name_length']
# create a container with long name
container_name = data_utils.arbitrary_string(size=max_length + 1)
- ex = self.assertRaises(exceptions.BadRequest,
- self.container_client.create_container,
- container_name)
+ ex = self.assertRaises(
+ exceptions.BadRequest,
+ self.container_client.update_container,
+ container_name)
self.assertIn('Container name length of ' + str(max_length + 1) +
' longer than ' + str(max_length), str(ex))
@@ -61,11 +62,13 @@
# that is longer than max.
max_length = self.constraints['max_meta_name_length']
container_name = data_utils.rand_name(name='TestContainer')
- metadata_name = data_utils.arbitrary_string(size=max_length + 1)
+ metadata_name = 'X-Container-Meta-' + data_utils.arbitrary_string(
+ size=max_length + 1)
metadata = {metadata_name: 'penguin'}
- ex = self.assertRaises(exceptions.BadRequest,
- self.container_client.create_container,
- container_name, metadata=metadata)
+ ex = self.assertRaises(
+ exceptions.BadRequest,
+ self.container_client.update_container,
+ container_name, **metadata)
self.assertIn('Metadata name too long', str(ex))
@decorators.attr(type=["negative"])
@@ -79,10 +82,11 @@
max_length = self.constraints['max_meta_value_length']
container_name = data_utils.rand_name(name='TestContainer')
metadata_value = data_utils.arbitrary_string(size=max_length + 1)
- metadata = {'animal': metadata_value}
- ex = self.assertRaises(exceptions.BadRequest,
- self.container_client.create_container,
- container_name, metadata=metadata)
+ metadata = {'X-Container-Meta-animal': metadata_value}
+ ex = self.assertRaises(
+ exceptions.BadRequest,
+ self.container_client.update_container,
+ container_name, **metadata)
self.assertIn('Metadata value longer than ' + str(max_length), str(ex))
@decorators.attr(type=["negative"])
@@ -97,11 +101,12 @@
container_name = data_utils.rand_name(name='TestContainer')
metadata = {}
for i in range(max_count + 1):
- metadata['animal-' + str(i)] = 'penguin'
+ metadata['X-Container-Meta-animal-' + str(i)] = 'penguin'
- ex = self.assertRaises(exceptions.BadRequest,
- self.container_client.create_container,
- container_name, metadata=metadata)
+ ex = self.assertRaises(
+ exceptions.BadRequest,
+ self.container_client.update_container,
+ container_name, **metadata)
self.assertIn('Too many metadata items; max ' + str(max_count),
str(ex))
@@ -120,9 +125,10 @@
# Attempts to update metadata using a nonexistent container name.
metadata = {'animal': 'penguin'}
- self.assertRaises(exceptions.NotFound,
- self.container_client.update_container_metadata,
- 'nonexistent_container_name', metadata)
+ self.assertRaises(
+ exceptions.NotFound,
+ self.container_client.create_update_or_delete_container_metadata,
+ 'nonexistent_container_name', create_update_metadata=metadata)
@decorators.attr(type=["negative"])
@decorators.idempotent_id('65387dbf-a0e2-4aac-9ddc-16eb3f1f69ba')
@@ -130,9 +136,10 @@
# Attempts to delete metadata using a nonexistent container name.
metadata = {'animal': 'penguin'}
- self.assertRaises(exceptions.NotFound,
- self.container_client.delete_container_metadata,
- 'nonexistent_container_name', metadata)
+ self.assertRaises(
+ exceptions.NotFound,
+ self.container_client.create_update_or_delete_container_metadata,
+ 'nonexistent_container_name', delete_metadata=metadata)
@decorators.attr(type=["negative"])
@decorators.idempotent_id('14331d21-1e81-420a-beea-19cb5e5207f5')
@@ -141,7 +148,7 @@
# that doesn't exist.
params = {'limit': 9999, 'format': 'json'}
self.assertRaises(exceptions.NotFound,
- self.container_client.list_container_contents,
+ self.container_client.list_container_objects,
'nonexistent_container_name', params)
@decorators.attr(type=["negative"])
@@ -155,7 +162,7 @@
self.assertHeaders(resp, 'Container', 'DELETE')
params = {'limit': 9999, 'format': 'json'}
self.assertRaises(exceptions.NotFound,
- self.container_client.list_container_contents,
+ self.container_client.list_container_objects,
container_name, params)
@decorators.attr(type=["negative"])
diff --git a/tempest/api/object_storage/test_container_staticweb.py b/tempest/api/object_storage/test_container_staticweb.py
index 92fa690..1243b83 100644
--- a/tempest/api/object_storage/test_container_staticweb.py
+++ b/tempest/api/object_storage/test_container_staticweb.py
@@ -34,10 +34,10 @@
cls.object_name, cls.object_data = cls.create_object(
cls.container_name)
- cls.container_client.update_container_metadata(
+ cls.container_client.create_update_or_delete_container_metadata(
cls.container_name,
- metadata=headers_public_read_acl,
- metadata_prefix="X-Container-")
+ create_update_metadata=headers_public_read_acl,
+ create_update_metadata_prefix="X-Container-")
@classmethod
def resource_cleanup(cls):
@@ -49,8 +49,8 @@
def test_web_index(self):
headers = {'web-index': self.object_name}
- self.container_client.update_container_metadata(
- self.container_name, metadata=headers)
+ self.container_client.create_update_or_delete_container_metadata(
+ self.container_name, create_update_metadata=headers)
# Maintain original headers, no auth added
self.account_client.auth_provider.set_alt_auth_data(
@@ -68,8 +68,9 @@
self.assertEqual(body, self.object_data)
# clean up before exiting
- self.container_client.update_container_metadata(self.container_name,
- {'web-index': ""})
+ self.container_client.create_update_or_delete_container_metadata(
+ self.container_name,
+ create_update_metadata={'web-index': ""})
_, body = self.container_client.list_container_metadata(
self.container_name)
@@ -80,8 +81,8 @@
def test_web_listing(self):
headers = {'web-listings': 'true'}
- self.container_client.update_container_metadata(
- self.container_name, metadata=headers)
+ self.container_client.create_update_or_delete_container_metadata(
+ self.container_name, create_update_metadata=headers)
# test GET on http://account_url/container_name
# we should retrieve a listing of objects
@@ -100,9 +101,9 @@
self.assertIn(self.object_name, body.decode())
# clean up before exiting
- self.container_client.update_container_metadata(self.container_name,
- {'web-listings': ""})
-
+ self.container_client.create_update_or_delete_container_metadata(
+ self.container_name,
+ create_update_metadata={'web-listings': ""})
_, body = self.container_client.list_container_metadata(
self.container_name)
self.assertNotIn('x-container-meta-web-listings', body)
@@ -113,8 +114,8 @@
headers = {'web-listings': 'true',
'web-listings-css': 'listings.css'}
- self.container_client.update_container_metadata(
- self.container_name, metadata=headers)
+ self.container_client.create_update_or_delete_container_metadata(
+ self.container_name, create_update_metadata=headers)
# Maintain original headers, no auth added
self.account_client.auth_provider.set_alt_auth_data(
@@ -136,8 +137,8 @@
headers = {'web-listings': 'true',
'web-error': self.object_name}
- self.container_client.update_container_metadata(
- self.container_name, metadata=headers)
+ self.container_client.create_update_or_delete_container_metadata(
+ self.container_name, create_update_metadata=headers)
# Create object to return when requested object not found
object_name_404 = "404" + self.object_name
diff --git a/tempest/api/object_storage/test_container_sync.py b/tempest/api/object_storage/test_container_sync.py
index 7665b48..042d288 100644
--- a/tempest/api/object_storage/test_container_sync.py
+++ b/tempest/api/object_storage/test_container_sync.py
@@ -102,7 +102,7 @@
while self.attempts > 0:
object_lists = []
for c_client, cont in zip(cont_client, self.containers):
- resp, object_list = c_client.list_container_contents(
+ resp, object_list = c_client.list_container_objects(
cont, params=params)
object_lists.append(dict(
(obj['name'], obj) for obj in object_list))
diff --git a/tempest/api/object_storage/test_object_expiry.py b/tempest/api/object_storage/test_object_expiry.py
index ed1be90..86f7c8c 100644
--- a/tempest/api/object_storage/test_object_expiry.py
+++ b/tempest/api/object_storage/test_object_expiry.py
@@ -40,10 +40,10 @@
def _test_object_expiry(self, metadata):
# update object metadata
resp, _ = \
- self.object_client.update_object_metadata(self.container_name,
- self.object_name,
- metadata,
- metadata_prefix='')
+ self.object_client.create_or_update_object_metadata(
+ self.container_name,
+ self.object_name,
+ headers=metadata)
# verify object metadata
resp, _ = \
self.object_client.list_object_metadata(self.container_name,
diff --git a/tempest/api/object_storage/test_object_services.py b/tempest/api/object_storage/test_object_services.py
index d3cdb72..acb578d 100644
--- a/tempest/api/object_storage/test_object_services.py
+++ b/tempest/api/object_storage/test_object_services.py
@@ -48,8 +48,9 @@
data_segments = [data + str(i) for i in range(segments)]
# uploading segments
for i in range(segments):
- self.object_client.create_object_segments(
- self.container_name, object_name, i, data_segments[i])
+ obj_name = "%s/%s" % (object_name, i)
+ self.object_client.create_object(
+ self.container_name, obj_name, data_segments[i])
return object_name, data_segments
@@ -184,12 +185,15 @@
# create object with transfer_encoding
object_name = data_utils.rand_name(name='TestObject')
data = data_utils.random_bytes(1024)
- _, _, resp_headers = self.object_client.put_object_with_chunk(
- container=self.container_name,
- name=object_name,
- contents=data_utils.chunkify(data, 512)
- )
- self.assertHeaders(resp_headers, 'Object', 'PUT')
+ headers = {'Transfer-Encoding': 'chunked'}
+ resp, _ = self.object_client.create_object(
+ self.container_name,
+ object_name,
+ data=data_utils.chunkify(data, 512),
+ headers=headers,
+ chunked=True)
+
+ self.assertHeaders(resp, 'Object', 'PUT')
# check uploaded content
_, body = self.object_client.get_object(self.container_name,
@@ -325,11 +329,10 @@
object_name, _ = self.create_object(self.container_name)
metadata = {'X-Object-Meta-test-meta': 'Meta'}
- resp, _ = self.object_client.update_object_metadata(
+ resp, _ = self.object_client.create_or_update_object_metadata(
self.container_name,
object_name,
- metadata,
- metadata_prefix='')
+ headers=metadata)
self.assertHeaders(resp, 'Object', 'POST')
resp, _ = self.object_client.list_object_metadata(
@@ -350,11 +353,10 @@
metadata=create_metadata)
update_metadata = {'X-Remove-Object-Meta-test-meta1': 'Meta1'}
- resp, _ = self.object_client.update_object_metadata(
+ resp, _ = self.object_client.create_or_update_object_metadata(
self.container_name,
object_name,
- update_metadata,
- metadata_prefix='')
+ headers=update_metadata)
self.assertHeaders(resp, 'Object', 'POST')
resp, _ = self.object_client.list_object_metadata(
@@ -375,11 +377,10 @@
update_metadata = {'X-Object-Meta-test-meta2': 'Meta2',
'X-Remove-Object-Meta-test-meta1': 'Meta1'}
- resp, _ = self.object_client.update_object_metadata(
+ resp, _ = self.object_client.create_or_update_object_metadata(
self.container_name,
object_name,
- update_metadata,
- metadata_prefix='')
+ headers=update_metadata)
self.assertHeaders(resp, 'Object', 'POST')
resp, _ = self.object_client.list_object_metadata(
@@ -403,11 +404,10 @@
metadata=None)
object_prefix = '%s/%s' % (self.container_name, object_name)
update_metadata = {'X-Object-Manifest': object_prefix}
- resp, _ = self.object_client.update_object_metadata(
+ resp, _ = self.object_client.create_or_update_object_metadata(
self.container_name,
object_name,
- update_metadata,
- metadata_prefix='')
+ headers=update_metadata)
self.assertHeaders(resp, 'Object', 'POST')
resp, _ = self.object_client.list_object_metadata(
@@ -422,11 +422,10 @@
object_name, _ = self.create_object(self.container_name)
update_metadata = {'X-Object-Meta-test-meta': ''}
- resp, _ = self.object_client.update_object_metadata(
+ resp, _ = self.object_client.create_or_update_object_metadata(
self.container_name,
object_name,
- update_metadata,
- metadata_prefix='')
+ headers=update_metadata)
self.assertHeaders(resp, 'Object', 'POST')
resp, _ = self.object_client.list_object_metadata(
@@ -447,11 +446,10 @@
metadata=create_metadata)
update_metadata = {'X-Remove-Object-Meta-test-meta': ''}
- resp, _ = self.object_client.update_object_metadata(
+ resp, _ = self.object_client.create_or_update_object_metadata(
self.container_name,
object_name,
- update_metadata,
- metadata_prefix='')
+ headers=update_metadata)
self.assertHeaders(resp, 'Object', 'POST')
resp, _ = self.object_client.list_object_metadata(
@@ -728,8 +726,13 @@
dst_object_name,
dst_data)
# copy source object to destination
- resp, _ = self.object_client.copy_object_in_same_container(
- self.container_name, src_object_name, dst_object_name)
+ headers = {}
+ headers['X-Copy-From'] = "%s/%s" % (str(self.container_name),
+ str(src_object_name))
+ resp, body = self.object_client.create_object(self.container_name,
+ dst_object_name,
+ data=None,
+ headers=headers)
self.assertHeaders(resp, 'Object', 'PUT')
# check data
@@ -749,8 +752,14 @@
# change the content type of the object
metadata = {'content-type': 'text/plain; charset=UTF-8'}
self.assertNotEqual(resp_tmp['content-type'], metadata['content-type'])
- resp, _ = self.object_client.copy_object_in_same_container(
- self.container_name, object_name, object_name, metadata)
+ headers = {}
+ headers['X-Copy-From'] = "%s/%s" % (str(self.container_name),
+ str(object_name))
+ resp, body = self.object_client.create_object(self.container_name,
+ object_name,
+ data=None,
+ metadata=metadata,
+ headers=headers)
self.assertHeaders(resp, 'Object', 'PUT')
# check the content type
@@ -786,12 +795,12 @@
def test_copy_object_across_containers(self):
# create a container to use as a source container
src_container_name = data_utils.rand_name(name='TestSourceContainer')
- self.container_client.create_container(src_container_name)
+ self.container_client.update_container(src_container_name)
self.containers.append(src_container_name)
# create a container to use as a destination container
dst_container_name = data_utils.rand_name(
name='TestDestinationContainer')
- self.container_client.create_container(dst_container_name)
+ self.container_client.update_container(dst_container_name)
self.containers.append(dst_container_name)
# create object in source container
object_name = data_utils.rand_name(name='Object')
@@ -801,16 +810,21 @@
# set object metadata
meta_key = data_utils.rand_name(name='test')
meta_value = data_utils.rand_name(name='MetaValue')
- orig_metadata = {meta_key: meta_value}
- resp, _ = self.object_client.update_object_metadata(src_container_name,
- object_name,
- orig_metadata)
+ orig_metadata = {'X-Object-Meta-' + meta_key: meta_value}
+ resp, _ = self.object_client.create_or_update_object_metadata(
+ src_container_name,
+ object_name,
+ headers=orig_metadata)
self.assertHeaders(resp, 'Object', 'POST')
# copy object from source container to destination container
- resp, _ = self.object_client.copy_object_across_containers(
- src_container_name, object_name, dst_container_name,
- object_name)
+ headers = {}
+ headers['X-Copy-From'] = "%s/%s" % (str(src_container_name),
+ str(object_name))
+ resp, body = self.object_client.create_object(dst_container_name,
+ object_name,
+ data=None,
+ headers=headers)
self.assertHeaders(resp, 'Object', 'PUT')
# check if object is present in destination container
@@ -897,8 +911,9 @@
data_segments = [data + str(i) for i in range(segments)]
# uploading segments
for i in range(segments):
- resp, _ = self.object_client.create_object_segments(
- self.container_name, object_name, i, data_segments[i])
+ obj_name = "%s/%s" % (object_name, i)
+ resp, _ = self.object_client.create_object(
+ self.container_name, obj_name, data_segments[i])
# creating a manifest file
metadata = {'X-Object-Manifest': '%s/%s/'
% (self.container_name, object_name)}
@@ -906,8 +921,8 @@
object_name, data='')
self.assertHeaders(resp, 'Object', 'PUT')
- resp, _ = self.object_client.update_object_metadata(
- self.container_name, object_name, metadata, metadata_prefix='')
+ resp, _ = self.object_client.create_or_update_object_metadata(
+ self.container_name, object_name, headers=metadata)
self.assertHeaders(resp, 'Object', 'POST')
resp, _ = self.object_client.list_object_metadata(
@@ -977,7 +992,7 @@
def setUp(self):
super(PublicObjectTest, self).setUp()
self.container_name = data_utils.rand_name(name='TestContainer')
- self.container_client.create_container(self.container_name)
+ self.container_client.update_container(self.container_name)
def tearDown(self):
self.delete_containers([self.container_name])
@@ -990,8 +1005,11 @@
# update container metadata to make it publicly readable
cont_headers = {'X-Container-Read': '.r:*,.rlistings'}
- resp_meta, body = self.container_client.update_container_metadata(
- self.container_name, metadata=cont_headers, metadata_prefix='')
+ resp_meta, body = (
+ self.container_client.create_update_or_delete_container_metadata(
+ self.container_name,
+ create_update_metadata=cont_headers,
+ create_update_metadata_prefix=''))
self.assertHeaders(resp_meta, 'Container', 'POST')
# create object
@@ -1025,9 +1043,10 @@
# make container public-readable and access an object in it using
# another user's credentials
cont_headers = {'X-Container-Read': '.r:*,.rlistings'}
- resp_meta, body = self.container_client.update_container_metadata(
- self.container_name, metadata=cont_headers,
- metadata_prefix='')
+ resp_meta, body = (
+ self.container_client.create_update_or_delete_container_metadata(
+ self.container_name, create_update_metadata=cont_headers,
+ create_update_metadata_prefix=''))
self.assertHeaders(resp_meta, 'Container', 'POST')
# create object
diff --git a/tempest/api/object_storage/test_object_slo.py b/tempest/api/object_storage/test_object_slo.py
index 65da63d..c66776e 100644
--- a/tempest/api/object_storage/test_object_slo.py
+++ b/tempest/api/object_storage/test_object_slo.py
@@ -172,6 +172,6 @@
# Check only the format of common headers with custom matcher
self.assertThat(resp, custom_matchers.AreAllWellFormatted())
- resp, body = self.container_client.list_container_contents(
+ resp, body = self.container_client.list_container_objects(
self.container_name)
self.assertEqual(int(resp['x-container-object-count']), 0)
diff --git a/tempest/api/object_storage/test_object_version.py b/tempest/api/object_storage/test_object_version.py
index 4799053..51b0a1d 100644
--- a/tempest/api/object_storage/test_object_version.py
+++ b/tempest/api/object_storage/test_object_version.py
@@ -51,18 +51,16 @@
def test_versioned_container(self):
# create container
vers_container_name = data_utils.rand_name(name='TestVersionContainer')
- resp, _ = self.container_client.create_container(
- vers_container_name)
+ resp, _ = self.container_client.update_container(vers_container_name)
self.containers.append(vers_container_name)
self.assertHeaders(resp, 'Container', 'PUT')
self.assertContainer(vers_container_name, '0', '0', 'Missing Header')
base_container_name = data_utils.rand_name(name='TestBaseContainer')
headers = {'X-versions-Location': vers_container_name}
- resp, _ = self.container_client.create_container(
+ resp, _ = self.container_client.update_container(
base_container_name,
- metadata=headers,
- metadata_prefix='')
+ **headers)
self.containers.append(base_container_name)
self.assertHeaders(resp, 'Container', 'PUT')
self.assertContainer(base_container_name, '0', '0',
diff --git a/tempest/api/volume/admin/test_groups.py b/tempest/api/volume/admin/test_groups.py
index baea37b..6b53d85 100644
--- a/tempest/api/volume/admin/test_groups.py
+++ b/tempest/api/volume/admin/test_groups.py
@@ -23,10 +23,7 @@
CONF = config.CONF
-class GroupsTest(base.BaseVolumeAdminTest):
- _api_version = 3
- min_microversion = '3.14'
- max_microversion = 'latest'
+class BaseGroupsTest(base.BaseVolumeAdminTest):
def _delete_group(self, grp_id, delete_volumes=True):
self.groups_client.delete_group(grp_id, delete_volumes)
@@ -37,8 +34,7 @@
self.groups_client.wait_for_resource_deletion(grp_id)
def _delete_group_snapshot(self, group_snapshot_id, grp_id):
- self.group_snapshots_client.delete_group_snapshot(
- group_snapshot_id)
+ self.group_snapshots_client.delete_group_snapshot(group_snapshot_id)
vols = self.volumes_client.list_volumes(detail=True)['volumes']
snapshots = self.snapshots_client.list_snapshots(
detail=True)['snapshots']
@@ -65,6 +61,12 @@
self.assertEqual(grp_name, grp['name'])
return grp
+
+class GroupsTest(BaseGroupsTest):
+ _api_version = 3
+ min_microversion = '3.14'
+ max_microversion = 'latest'
+
@decorators.idempotent_id('4b111d28-b73d-4908-9bd2-03dc2992e4d4')
def test_group_create_show_list_delete(self):
# Create volume type
@@ -106,16 +108,16 @@
self.assertEqual(grp2_id, grp2['id'])
# Get all groups with detail
- grps = self.groups_client.list_groups(
- detail=True)['groups']
- filtered_grps = [g for g in grps if g['id'] in [grp1_id, grp2_id]]
- self.assertEqual(2, len(filtered_grps))
- for grp in filtered_grps:
- self.assertEqual([volume_type['id']], grp['volume_types'])
- self.assertEqual(group_type['id'], grp['group_type'])
+ grps = self.groups_client.list_groups(detail=True)['groups']
+ for grp_id in [grp1_id, grp2_id]:
+ filtered_grps = [g for g in grps if g['id'] == grp_id]
+ self.assertEqual(1, len(filtered_grps))
+ self.assertEqual([volume_type['id']],
+ filtered_grps[0]['volume_types'])
+ self.assertEqual(group_type['id'],
+ filtered_grps[0]['group_type'])
- vols = self.volumes_client.list_volumes(
- detail=True)['volumes']
+ vols = self.volumes_client.list_volumes(detail=True)['volumes']
filtered_vols = [v for v in vols if v['id'] in [vol1_id]]
self.assertEqual(1, len(filtered_vols))
for vol in filtered_vols:
@@ -126,8 +128,7 @@
self._delete_group(grp1_id)
# grp2 is empty so delete_volumes flag can be set to False
self._delete_group(grp2_id, delete_volumes=False)
- grps = self.groups_client.list_groups(
- detail=True)['groups']
+ grps = self.groups_client.list_groups(detail=True)['groups']
self.assertEmpty(grps)
@decorators.idempotent_id('1298e537-f1f0-47a3-a1dd-8adec8168897')
@@ -151,6 +152,9 @@
self.group_snapshots_client.create_group_snapshot(
group_id=grp['id'],
name=group_snapshot_name)['group_snapshot'])
+ self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+ self._delete_group_snapshot,
+ group_snapshot['id'], grp['id'])
snapshots = self.snapshots_client.list_snapshots(
detail=True)['snapshots']
for snap in snapshots:
@@ -167,18 +171,20 @@
group_snapshot['id'])['group_snapshot']
self.assertEqual(group_snapshot_name, group_snapshot['name'])
- # Get all group snapshots with detail
- group_snapshots = (
- self.group_snapshots_client.list_group_snapshots(
- detail=True)['group_snapshots'])
+ # Get all group snapshots with details, check some detail-specific
+ # elements, and look for the created group snapshot
+ group_snapshots = (self.group_snapshots_client.list_group_snapshots(
+ detail=True)['group_snapshots'])
+ for grp_snapshot in group_snapshots:
+ self.assertIn('created_at', grp_snapshot)
+ self.assertIn('group_id', grp_snapshot)
self.assertIn((group_snapshot['name'], group_snapshot['id']),
[(m['name'], m['id']) for m in group_snapshots])
# Delete group snapshot
self._delete_group_snapshot(group_snapshot['id'], grp['id'])
- group_snapshots = (
- self.group_snapshots_client.list_group_snapshots(
- detail=True)['group_snapshots'])
+ group_snapshots = (self.group_snapshots_client.list_group_snapshots()
+ ['group_snapshots'])
self.assertEmpty(group_snapshots)
@decorators.idempotent_id('eff52c70-efc7-45ed-b47a-4ad675d09b81')
@@ -212,14 +218,12 @@
waiters.wait_for_volume_resource_status(
self.snapshots_client, snap['id'], 'available')
waiters.wait_for_volume_resource_status(
- self.group_snapshots_client,
- group_snapshot['id'], 'available')
+ self.group_snapshots_client, group_snapshot['id'], 'available')
# Create Group from Group snapshot
grp_name2 = data_utils.rand_name('Group_from_snap')
grp2 = self.groups_client.create_group_from_source(
- group_snapshot_id=group_snapshot['id'],
- name=grp_name2)['group']
+ group_snapshot_id=group_snapshot['id'], name=grp_name2)['group']
self.addCleanup(self._delete_group, grp2['id'])
self.assertEqual(grp_name2, grp2['name'])
vols = self.volumes_client.list_volumes(detail=True)['volumes']
@@ -250,8 +254,7 @@
source_group_id=grp['id'], name=grp_name2)['group']
self.addCleanup(self._delete_group, grp2['id'])
self.assertEqual(grp_name2, grp2['name'])
- vols = self.volumes_client.list_volumes(
- detail=True)['volumes']
+ vols = self.volumes_client.list_volumes(detail=True)['volumes']
for vol in vols:
if vol['group_id'] == grp2['id']:
waiters.wait_for_volume_resource_status(
@@ -296,12 +299,8 @@
self.assertEqual(new_desc, grp['description'])
# Get volumes in the group
- vols = self.volumes_client.list_volumes(
- detail=True)['volumes']
- grp_vols = []
- for vol in vols:
- if vol['group_id'] == grp['id']:
- grp_vols.append(vol)
+ vols = self.volumes_client.list_volumes(detail=True)['volumes']
+ grp_vols = [v for v in vols if v['group_id'] == grp['id']]
self.assertEqual(1, len(grp_vols))
# Add a volume to the group
@@ -313,10 +312,82 @@
self.groups_client, grp['id'], 'available')
# Get volumes in the group
- vols = self.volumes_client.list_volumes(
- detail=True)['volumes']
- grp_vols = []
- for vol in vols:
- if vol['group_id'] == grp['id']:
- grp_vols.append(vol)
+ vols = self.volumes_client.list_volumes(detail=True)['volumes']
+ grp_vols = [v for v in vols if v['group_id'] == grp['id']]
self.assertEqual(2, len(grp_vols))
+
+
+class GroupsV319Test(BaseGroupsTest):
+ _api_version = 3
+ min_microversion = '3.19'
+ max_microversion = 'latest'
+
+ @decorators.idempotent_id('3b42c9b9-c984-4444-816e-ca2e1ed30b40')
+ def test_reset_group_snapshot_status(self):
+ # Create volume type
+ volume_type = self.create_volume_type()
+
+ # Create group type
+ group_type = self.create_group_type()
+
+ # Create group
+ group = self._create_group(group_type, volume_type)
+
+ # Create volume
+ volume = self.create_volume(volume_type=volume_type['id'],
+ group_id=group['id'])
+
+ # Create group snapshot
+ group_snapshot_name = data_utils.rand_name('group_snapshot')
+ group_snapshot = (self.group_snapshots_client.create_group_snapshot(
+ group_id=group['id'], name=group_snapshot_name)['group_snapshot'])
+ self.addCleanup(self._delete_group_snapshot,
+ group_snapshot['id'], group['id'])
+ snapshots = self.snapshots_client.list_snapshots(
+ detail=True)['snapshots']
+ for snap in snapshots:
+ if volume['id'] == snap['volume_id']:
+ waiters.wait_for_volume_resource_status(
+ self.snapshots_client, snap['id'], 'available')
+ waiters.wait_for_volume_resource_status(
+ self.group_snapshots_client, group_snapshot['id'], 'available')
+
+ # Reset group snapshot status
+ self.addCleanup(waiters.wait_for_volume_resource_status,
+ self.group_snapshots_client,
+ group_snapshot['id'], 'available')
+ self.addCleanup(
+ self.admin_group_snapshots_client.reset_group_snapshot_status,
+ group_snapshot['id'], 'available')
+ for status in ['creating', 'available', 'error']:
+ self.admin_group_snapshots_client.reset_group_snapshot_status(
+ group_snapshot['id'], status)
+ waiters.wait_for_volume_resource_status(
+ self.group_snapshots_client, group_snapshot['id'], status)
+
+
+class GroupsV320Test(BaseGroupsTest):
+ _api_version = 3
+ min_microversion = '3.20'
+ max_microversion = 'latest'
+
+ @decorators.idempotent_id('b20c696b-0cbc-49a5-8b3a-b1fb9338f45c')
+ def test_reset_group_status(self):
+ # Create volume type
+ volume_type = self.create_volume_type()
+
+ # Create group type
+ group_type = self.create_group_type()
+
+ # Create group
+ group = self._create_group(group_type, volume_type)
+
+ # Reset group status
+ self.addCleanup(waiters.wait_for_volume_resource_status,
+ self.groups_client, group['id'], 'available')
+ self.addCleanup(self.admin_groups_client.reset_group_status,
+ group['id'], 'available')
+ for status in ['creating', 'available', 'error']:
+ self.admin_groups_client.reset_group_status(group['id'], status)
+ waiters.wait_for_volume_resource_status(
+ self.groups_client, group['id'], status)
diff --git a/tempest/api/volume/admin/test_user_messages.py b/tempest/api/volume/admin/test_user_messages.py
old mode 100755
new mode 100644
diff --git a/tempest/api/volume/admin/test_volume_quota_classes.py b/tempest/api/volume/admin/test_volume_quota_classes.py
index f551575..75dca41 100644
--- a/tempest/api/volume/admin/test_volume_quota_classes.py
+++ b/tempest/api/volume/admin/test_volume_quota_classes.py
@@ -19,6 +19,7 @@
from testtools import matchers
from tempest.api.volume import base
+from tempest.common import identity
from tempest.common import tempest_fixtures as fixtures
from tempest.lib.common.utils import data_utils
from tempest.lib import decorators
@@ -92,9 +93,10 @@
# Verify a new project's default quotas.
project_name = data_utils.rand_name('quota_class_tenant')
description = data_utils.rand_name('desc_')
- project_id = self.identity_utils.create_project(
+ project_id = identity.identity_utils(self.os_admin).create_project(
name=project_name, description=description)['id']
- self.addCleanup(self.identity_utils.delete_project, project_id)
+ self.addCleanup(identity.identity_utils(self.os_admin).delete_project,
+ project_id)
default_quotas = self.admin_quotas_client.show_default_quota_set(
project_id)['quota_set']
self.assertThat(default_quotas.items(),
diff --git a/tempest/api/volume/admin/test_volume_quotas.py b/tempest/api/volume/admin/test_volume_quotas.py
index 754104e..42bfcd6 100644
--- a/tempest/api/volume/admin/test_volume_quotas.py
+++ b/tempest/api/volume/admin/test_volume_quotas.py
@@ -13,12 +13,14 @@
# under the License.
from tempest.api.volume import base
+from tempest.common import identity
from tempest.common import tempest_fixtures as fixtures
from tempest.common import waiters
from tempest.lib.common.utils import data_utils
from tempest.lib import decorators
-QUOTA_KEYS = ['gigabytes', 'snapshots', 'volumes', 'backups']
+QUOTA_KEYS = ['gigabytes', 'snapshots', 'volumes', 'backups',
+ 'backup_gigabytes', 'per_volume_gigabytes']
QUOTA_USAGE_KEYS = ['reserved', 'limit', 'in_use']
@@ -66,7 +68,9 @@
new_quota_set = {'gigabytes': 1009,
'volumes': 11,
'snapshots': 11,
- 'backups': 11}
+ 'backups': 11,
+ 'backup_gigabytes': 1009,
+ 'per_volume_gigabytes': 1009}
# Update limits for all quota resources
quota_set = self.admin_quotas_client.update_quota_set(
@@ -100,7 +104,7 @@
volume = self.create_volume()
self.addCleanup(self.delete_volume,
- self.admin_volume_client, volume['id'])
+ self.volumes_client, volume['id'])
new_quota_usage = self.admin_quotas_client.show_quota_set(
self.demo_tenant_id, params={'usage': True})['quota_set']
@@ -117,10 +121,11 @@
# Admin can delete the resource quota set for a project
project_name = data_utils.rand_name('quota_tenant')
description = data_utils.rand_name('desc_')
- project = self.identity_utils.create_project(project_name,
- description=description)
+ project = identity.identity_utils(self.os_admin).create_project(
+ project_name, description=description)
project_id = project['id']
- self.addCleanup(self.identity_utils.delete_project, project_id)
+ self.addCleanup(identity.identity_utils(self.os_admin).delete_project,
+ project_id)
quota_set_default = self.admin_quotas_client.show_default_quota_set(
project_id)['quota_set']
volume_default = quota_set_default['volumes']
diff --git a/tempest/api/volume/admin/test_volume_types_extra_specs.py b/tempest/api/volume/admin/test_volume_types_extra_specs.py
index b5a2fb7..730acdf 100644
--- a/tempest/api/volume/admin/test_volume_types_extra_specs.py
+++ b/tempest/api/volume/admin/test_volume_types_extra_specs.py
@@ -46,14 +46,32 @@
self.volume_type['id'], extra_specs)['extra_specs']
self.assertEqual(extra_specs, body,
"Volume type extra spec incorrectly created")
+
+ # Only update an extra spec
spec_key = "spec2"
extra_spec = {spec_key: "val2"}
body = self.admin_volume_types_client.update_volume_type_extra_specs(
self.volume_type['id'], spec_key, extra_spec)
self.assertIn(spec_key, body)
+ self.assertEqual(extra_spec[spec_key], body[spec_key])
+ body = self.admin_volume_types_client.show_volume_type_extra_specs(
+ self.volume_type['id'], spec_key)
+ self.assertIn(spec_key, body)
self.assertEqual(extra_spec[spec_key], body[spec_key],
"Volume type extra spec incorrectly updated")
+ # Update an existing extra spec and create a new extra spec
+ extra_specs = {spec_key: "val3", "spec4": "val4"}
+ body = self.admin_volume_types_client.create_volume_type_extra_specs(
+ self.volume_type['id'], extra_specs)['extra_specs']
+ self.assertEqual(extra_specs, body)
+ body = self.admin_volume_types_client.list_volume_types_extra_specs(
+ self.volume_type['id'])['extra_specs']
+ for key in extra_specs:
+ self.assertIn(key, body)
+ self.assertEqual(extra_specs[key], body[key],
+ "Volume type extra spec incorrectly created")
+
@decorators.idempotent_id('d4772798-601f-408a-b2a5-29e8a59d1220')
def test_volume_type_extra_spec_create_get_delete(self):
# Create/Get/Delete volume type extra spec.
diff --git a/tempest/api/volume/admin/test_volumes_actions.py b/tempest/api/volume/admin/test_volumes_actions.py
index 8d09217..3e0deef 100644
--- a/tempest/api/volume/admin/test_volumes_actions.py
+++ b/tempest/api/volume/admin/test_volumes_actions.py
@@ -37,7 +37,7 @@
@decorators.idempotent_id('d063f96e-a2e0-4f34-8b8a-395c42de1845')
def test_volume_reset_status(self):
- # test volume reset status : available->error->available
+ # test volume reset status : available->error->available->maintenance
volume = self.create_volume()
self.addCleanup(waiters.wait_for_volume_resource_status,
self.volumes_client, volume['id'], 'available')
diff --git a/tempest/api/volume/test_availability_zone.py b/tempest/api/volume/test_availability_zone.py
index d0a87db..0b6ee38 100644
--- a/tempest/api/volume/test_availability_zone.py
+++ b/tempest/api/volume/test_availability_zone.py
@@ -20,14 +20,10 @@
class AvailabilityZoneTestJSON(base.BaseVolumeTest):
"""Tests Availability Zone API List"""
- @classmethod
- def setup_clients(cls):
- super(AvailabilityZoneTestJSON, cls).setup_clients()
- cls.client = cls.availability_zone_client
-
@decorators.idempotent_id('01f1ae88-eba9-4c6b-a011-6f7ace06b725')
def test_get_availability_zone_list(self):
# List of availability zone
- availability_zone = (self.client.list_availability_zones()
- ['availabilityZoneInfo'])
+ availability_zone = (
+ self.availability_zone_client.list_availability_zones()
+ ['availabilityZoneInfo'])
self.assertNotEmpty(availability_zone)
diff --git a/tempest/api/volume/test_volumes_extend.py b/tempest/api/volume/test_volumes_extend.py
index 1eb76a0..b73bdf2 100644
--- a/tempest/api/volume/test_volumes_extend.py
+++ b/tempest/api/volume/test_volumes_extend.py
@@ -13,12 +13,16 @@
# License for the specific language governing permissions and limitations
# under the License.
+import time
+
import testtools
from tempest.api.volume import base
+from tempest.common import utils
from tempest.common import waiters
from tempest import config
from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
CONF = config.CONF
@@ -53,3 +57,130 @@
resized_volume = self.volumes_client.show_volume(
volume['id'])['volume']
self.assertEqual(extend_size, resized_volume['size'])
+
+
+class VolumesExtendAttachedTest(base.BaseVolumeTest):
+ """Tests extending the size of an attached volume."""
+
+ # 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
+ # server instance, but since the event details can contain error messages
+ # and tracebacks, like an instance fault, those are not viewable by
+ # non-admins. This is obviously not a great user experience since the user
+ # may not know when the operation is actually complete. A microversion in
+ # the compute API will be added so that non-admins can see instance action
+ # events but will continue to hide the traceback field.
+ # TODO(mriedem): Change this to not rely on the admin user to get the event
+ # details once that microversion is available in Nova.
+ credentials = ['primary', 'admin']
+
+ _api_version = 3
+ # NOTE(mriedem): The minimum required volume API version is 3.42 and the
+ # minimum required compute API microversion is 2.51, but the compute call
+ # is implicit - Cinder calls Nova at that microversion, Tempest does not.
+ min_microversion = '3.42'
+
+ @classmethod
+ def setup_clients(cls):
+ super(VolumesExtendAttachedTest, cls).setup_clients()
+ cls.admin_servers_client = cls.os_admin.servers_client
+
+ def _find_extend_volume_instance_action(self, server_id):
+ actions = self.servers_client.list_instance_actions(
+ server_id)['instanceActions']
+ for action in actions:
+ if action['action'] == 'extend_volume':
+ return action
+
+ def _find_extend_volume_instance_action_finish_event(self, action):
+ # This has to be called by an admin client otherwise
+ # the events don't show up.
+ action = self.admin_servers_client.show_instance_action(
+ action['instance_uuid'], action['request_id'])['instanceAction']
+ for event in action['events']:
+ if (event['event'] == 'compute_extend_volume' and
+ event['finish_time']):
+ return event
+
+ @decorators.idempotent_id('301f5a30-1c6f-4ea0-be1a-91fd28d44354')
+ @testtools.skipUnless(CONF.volume_feature_enabled.extend_attached_volume,
+ "Attached volume extend is disabled.")
+ @utils.services('compute')
+ def test_extend_attached_volume(self):
+ """This is a happy path test which does the following:
+
+ * Create a volume at the configured volume_size.
+ * Create a server instance.
+ * Attach the volume to the server.
+ * Wait for the volume status to be "in-use".
+ * Extend the size of the volume and wait for the volume status to go
+ back to "in-use".
+ * Assert the volume size change is reflected in the volume API.
+ * Wait for the "compute_extend_volume" instance action event to show
+ up in the compute API with the success or failure status. We fail
+ if we timeout waiting for the instance action event to show up, or
+ if the action on the server fails.
+ """
+ # Create a test volume. Will be automatically cleaned up on teardown.
+ volume = self.create_volume()
+ # Create a test server. Will be automatically cleaned up on teardown.
+ server = self.create_server()
+ # Attach the volume to the server and wait for the volume status to be
+ # "in-use".
+ self.attach_volume(server['id'], volume['id'])
+ # Extend the size of the volume. If this is successful, the volume API
+ # will change the status on the volume to "extending" before doing an
+ # RPC cast to the volume manager on the backend. Note that we multiply
+ # the size of the volume since certain Cinder backends, e.g. ScaleIO,
+ # require multiples of 8GB.
+ extend_size = volume['size'] * 2
+ self.volumes_client.extend_volume(volume['id'], new_size=extend_size)
+ # The volume status should go back to in-use since it is still attached
+ # to the server instance.
+ waiters.wait_for_volume_resource_status(self.volumes_client,
+ volume['id'], 'in-use')
+ # Assert that the volume size has changed in the volume API.
+ volume = self.volumes_client.show_volume(volume['id'])['volume']
+ self.assertEqual(extend_size, volume['size'])
+ # Now we wait for the "compute_extend_volume" instance action event
+ # to show up for the server instance. This is our indication that the
+ # asynchronous operation is complete on the compute side.
+ start_time = int(time.time())
+ timeout = self.servers_client.build_timeout
+ action = self._find_extend_volume_instance_action(server['id'])
+ while action is None and int(time.time()) - start_time < timeout:
+ time.sleep(self.servers_client.build_interval)
+ action = self._find_extend_volume_instance_action(server['id'])
+
+ if action is None:
+ msg = ("Timed out waiting to get 'extend_volume' instance action "
+ "record for server %(server)s after %(timeout)s seconds." %
+ {'server': server['id'], 'timeout': timeout})
+ raise lib_exc.TimeoutException(msg)
+
+ # Now that we found the extend_volume instance action, we can wait for
+ # the compute_extend_volume instance action event to show up to
+ # indicate the operation is complete.
+ start_time = int(time.time())
+ event = self._find_extend_volume_instance_action_finish_event(action)
+ while event is None and int(time.time()) - start_time < timeout:
+ time.sleep(self.servers_client.build_interval)
+ event = self._find_extend_volume_instance_action_finish_event(
+ action)
+
+ if event is None:
+ msg = ("Timed out waiting to get 'compute_extend_volume' instance "
+ "action event record for server %(server)s and request "
+ "%(request_id)s after %(timeout)s seconds." %
+ {'server': server['id'],
+ 'request_id': action['request_id'],
+ 'timeout': timeout})
+ raise lib_exc.TimeoutException(msg)
+
+ # Finally, assert that the action completed successfully.
+ self.assertTrue(
+ event['result'].lower() == 'success',
+ "Unexpected compute_extend_volume result '%(result)s' for request "
+ "%(request_id)s." %
+ {'result': event['result'],
+ 'request_id': action['request_id']})
diff --git a/tempest/clients.py b/tempest/clients.py
index e617c3c..ca205c8 100644
--- a/tempest/clients.py
+++ b/tempest/clients.py
@@ -17,7 +17,6 @@
from tempest.lib import auth
from tempest.lib import exceptions as lib_exc
from tempest.lib.services import clients
-from tempest.services import object_storage
CONF = config.CONF
@@ -25,8 +24,6 @@
class Manager(clients.ServiceClients):
"""Top level manager for OpenStack tempest clients"""
- default_params = config.service_client_config()
-
def __init__(self, credentials, scope='project'):
"""Initialization of Manager class.
@@ -47,6 +44,10 @@
self._set_object_storage_clients()
self._set_image_clients()
self._set_network_clients()
+ # TODO(andreaf) This is maintained for backward compatibility
+ # with plugins, but it should removed eventually, since it was
+ # never a stable interface and it's not useful anyways
+ self.default_params = config.service_client_config()
def _set_network_clients(self):
self.network_agents_client = self.network.AgentsClient()
@@ -281,21 +282,11 @@
self.snapshots_client_latest = self.snapshots_v3_client
def _set_object_storage_clients(self):
- # NOTE(andreaf) Load configuration from config. Once object storage
- # is in lib, configuration will be pulled directly from the registry
- # and this will not be required anymore.
- params = config.service_client_config('object-storage')
-
- self.account_client = object_storage.AccountClient(self.auth_provider,
- **params)
- self.bulk_client = object_storage.BulkMiddlewareClient(
- self.auth_provider, **params)
- self.capabilities_client = object_storage.CapabilitiesClient(
- self.auth_provider, **params)
- self.container_client = object_storage.ContainerClient(
- self.auth_provider, **params)
- self.object_client = object_storage.ObjectClient(self.auth_provider,
- **params)
+ self.account_client = self.object_storage.AccountClient()
+ self.bulk_client = self.object_storage.BulkMiddlewareClient()
+ self.capabilities_client = self.object_storage.CapabilitiesClient()
+ self.container_client = self.object_storage.ContainerClient()
+ self.object_client = self.object_storage.ObjectClient()
def get_auth_provider_class(credentials):
diff --git a/tempest/cmd/account_generator.py b/tempest/cmd/account_generator.py
index 8636405..92eae02 100755
--- a/tempest/cmd/account_generator.py
+++ b/tempest/cmd/account_generator.py
@@ -15,18 +15,18 @@
# under the License.
"""
-Utility for creating **accounts.yaml** file for concurrent test runs.
+Utility for creating ``accounts.yaml`` file for concurrent test runs.
Creates one primary user, one alt user, one swift admin, one stack owner
and one admin (optionally) for each concurrent thread. The utility creates
-user for each tenant. The **accounts.yaml** file will be valid and contain
+user for each tenant. The ``accounts.yaml`` file will be valid and contain
credentials for created users, so each user will be in separate tenant and
have the username, tenant_name, password and roles.
-**Usage:** ``tempest account-generator [-h] [OPTIONS] accounts_file.yaml``.
+**Usage:** ``tempest account-generator [-h] [OPTIONS] accounts_file.yaml``
Positional Arguments
--------------------
-**accounts_file.yaml** (Required) Provide an output accounts yaml file. Utility
+``accounts_file.yaml`` (Required) Provide an output accounts yaml file. Utility
creates a .yaml file in the directory where the command is ran. The appropriate
name for the file is *accounts.yaml* and it should be placed in *tempest/etc*
directory.
@@ -40,55 +40,62 @@
You're probably familiar with these, but just to remind:
-======== ======================== ====================
-Param CLI Environment Variable
-======== ======================== ====================
-Username --os-username OS_USERNAME
-Password --os-password OS_PASSWORD
-Project --os-project-name OS_PROJECT_NAME
-Tenant --os-tenant-name (depr.) OS_TENANT_NAME
-Domain --os-domain-name OS_DOMAIN_NAME
-======== ======================== ====================
+======== ============================ ====================
+Param CLI Environment Variable
+======== ============================ ====================
+Username ``--os-username`` OS_USERNAME
+Password ``--os-password`` OS_PASSWORD
+Project ``--os-project-name`` OS_PROJECT_NAME
+Tenant ``--os-tenant-name`` (depr.) OS_TENANT_NAME
+Domain ``--os-domain-name`` OS_DOMAIN_NAME
+======== ============================ ====================
Optional Arguments
------------------
-**-h**, **--help** (Optional) Shows help message with the description of
-utility and its arguments, and exits.
+* ``-h, --help`` (Optional) Shows help message with the description of
+ utility and its arguments, and exits.
-**c /etc/tempest.conf**, **--config-file /etc/tempest.conf** (Optional) Path to
-tempest config file.
+* ``-c, --config-file /etc/tempest.conf`` (Optional) Path
+ to tempest config file. If not specified, it searches for tempest.conf in
+ these locations:
-**--os-username <auth-user-name>** (Optional) Name used for authentication with
-the OpenStack Identity service. Defaults to env[OS_USERNAME]. Note: User should
-have permissions to create new user accounts and tenants.
+ - ./etc/
+ - /etc/tempest
+ - ~/.tempest/
+ - ~/
+ - /etc/
-**--os-password <auth-password>** (Optional) Password used for authentication
-with the OpenStack Identity service. Defaults to env[OS_PASSWORD].
+* ``--os-username <auth-user-name>`` (Optional) Name used for authentication
+ with the OpenStack Identity service. Defaults to env[OS_USERNAME]. Note: User
+ should have permissions to create new user accounts and tenants.
-**--os-project-name <auth-project-name>** (Optional) Project to request
-authorization on. Defaults to env[OS_PROJECT_NAME].
+* ``--os-password <auth-password>`` (Optional) Password used for authentication
+ with the OpenStack Identity service. Defaults to env[OS_PASSWORD].
-**--os-tenant-name <auth-tenant-name>** (Optional, deprecated) Tenant to
-request authorization on. Defaults to env[OS_TENANT_NAME].
+* ``--os-project-name <auth-project-name>`` (Optional) Project to request
+ authorization on. Defaults to env[OS_PROJECT_NAME].
-**--os-domain-name <auth-domain-name>** (Optional) Domain the user and project
-belong to. Defaults to env[OS_DOMAIN_NAME].
+* ``--os-tenant-name <auth-tenant-name>`` (Optional, deprecated) Tenant to
+ request authorization on. Defaults to env[OS_TENANT_NAME].
-**--tag TAG** (Optional) Resources tag. Each created resource (user, project)
-will have the prefix with the given TAG in its name. Using tag is recommended
-for the further using, cleaning resources.
+* ``--os-domain-name <auth-domain-name>`` (Optional) Domain the user and
+ project belong to. Defaults to env[OS_DOMAIN_NAME].
-**-r CONCURRENCY**, **--concurrency CONCURRENCY** (Required) Concurrency count
-(default: 1). The number of accounts required can be estimated as
-CONCURRENCY x 2. Each user provided in *accounts.yaml* file will be in
-a different tenant. This is required to provide isolation between test for
-running in parallel.
+* ``--tag TAG`` (Optional) Resources tag. Each created resource (user, project)
+ will have the prefix with the given TAG in its name. Using tag is recommended
+ for the further using, cleaning resources.
-**--with-admin** (Optional) Creates admin for each concurrent group
-(default: False).
+* ``-r, --concurrency CONCURRENCY`` (Optional) Concurrency count
+ (default: 1). The number of accounts required can be estimated as
+ CONCURRENCY x 2. Each user provided in *accounts.yaml* file will be in
+ a different tenant. This is required to provide isolation between test for
+ running in parallel.
-**-i VERSION**, **--identity-version VERSION** (Optional) Provisions accounts
-using the specified version of the identity API. (default: '3').
+* ``--with-admin`` (Optional) Creates admin for each concurrent group
+ (default: False).
+
+* ``-i, --identity-version VERSION`` (Optional) Provisions accounts
+ using the specified version of the identity API. (default: '3').
To see help on specific argument, please do: ``tempest account-generator
[OPTIONS] <accounts_file.yaml> -h``.
diff --git a/tempest/cmd/cleanup.py b/tempest/cmd/cleanup.py
index a128b3f..29abd49 100644
--- a/tempest/cmd/cleanup.py
+++ b/tempest/cmd/cleanup.py
@@ -28,45 +28,48 @@
Example Run
-----------
-**WARNING: If step 1 is skipped in the example below, the cleanup procedure
-may delete resources that existed in the cloud before the test run. This
-may cause an unwanted destruction of cloud resources, so use caution with
-this command.**
+.. warning::
-``$ tempest cleanup --init-saved-state``
+ If step 1 is skipped in the example below, the cleanup procedure
+ may delete resources that existed in the cloud before the test run. This
+ may cause an unwanted destruction of cloud resources, so use caution with
+ this command.
-``$ # Actual running of Tempest tests``
+ Examples::
-``$ tempest cleanup``
+ $ tempest cleanup --init-saved-state
+ $ # Actual running of Tempest tests
+ $ tempest cleanup
Runtime Arguments
-----------------
-**--init-saved-state**: Initializes the saved state of the OpenStack deployment
-and will output a ``saved_state.json`` file containing resources from your
-deployment that will be preserved from the cleanup command. This should be
-done prior to running Tempest tests.
+* ``--init-saved-state``: Initializes the saved state of the OpenStack
+ deployment and will output a ``saved_state.json`` file containing resources
+ from your deployment that will be preserved from the cleanup command. This
+ should be done prior to running Tempest tests.
-**--delete-tempest-conf-objects**: If option is present, then the command will
-delete the admin project in addition to the resources associated with them on
-clean up. If option is not present, the command will delete the resources
-associated with the Tempest and alternate Tempest users and projects but will
-not delete the projects themselves.
+* ``--delete-tempest-conf-objects``: If option is present, then the command
+ will delete the admin project in addition to the resources associated with
+ them on clean up. If option is not present, the command will delete the
+ resources associated with the Tempest and alternate Tempest users and
+ projects but will not delete the projects themselves.
-**--dry-run**: Creates a report (``./dry_run.json``) of the projects that will
-be cleaned up (in the ``_tenants_to_clean`` dictionary [1]_) and the global
-objects that will be removed (domains, flavors, images, roles, projects,
-and users). Once the cleanup command is executed (e.g. run without
-parameters), running it again with **--dry-run** should yield an empty report.
+* ``--dry-run``: Creates a report (``./dry_run.json``) of the projects that
+ will be cleaned up (in the ``_projects_to_clean`` dictionary [1]_) and the
+ global objects that will be removed (domains, flavors, images, roles,
+ projects, and users). Once the cleanup command is executed (e.g. run without
+ parameters), running it again with ``--dry-run`` should yield an empty
+ report.
-**--help**: Print the help text for the command and parameters.
+* ``--help``: Print the help text for the command and parameters.
-.. [1] The ``_tenants_to_clean`` dictionary in ``dry_run.json`` lists the
+.. [1] The ``_projects_to_clean`` dictionary in ``dry_run.json`` lists the
projects that ``tempest cleanup`` will loop through to delete child
objects, but the command will, by default, not delete the projects
- themselves. This may differ from the ``tenants`` list as you can clean
+ themselves. This may differ from the ``projects`` list as you can clean
the Tempest and alternate Tempest users and projects but they will not be
- deleted unless the **--delete-tempest-conf-objects** flag is used to
+ deleted unless the ``--delete-tempest-conf-objects`` flag is used to
force their deletion.
"""
@@ -111,13 +114,13 @@
self.admin_id = ""
self.admin_role_id = ""
- self.admin_tenant_id = ""
+ self.admin_project_id = ""
self._init_admin_ids()
self.admin_role_added = []
# available services
- self.tenant_services = cleanup_service.get_tenant_cleanup_services()
+ self.project_services = cleanup_service.get_project_cleanup_services()
self.global_services = cleanup_service.get_global_cleanup_services()
if parsed_args.init_saved_state:
@@ -133,24 +136,24 @@
is_save_state = False
if is_dry_run:
- self.dry_run_data["_tenants_to_clean"] = {}
+ self.dry_run_data["_projects_to_clean"] = {}
admin_mgr = self.admin_mgr
- # Always cleanup tempest and alt tempest tenants unless
+ # Always cleanup tempest and alt tempest projects unless
# they are in saved state json. Therefore is_preserve is False
kwargs = {'data': self.dry_run_data,
'is_dry_run': is_dry_run,
'saved_state_json': self.json_data,
'is_preserve': False,
'is_save_state': is_save_state}
- tenant_service = cleanup_service.TenantService(admin_mgr, **kwargs)
- tenants = tenant_service.list()
- print("Process %s tenants" % len(tenants))
+ project_service = cleanup_service.ProjectService(admin_mgr, **kwargs)
+ projects = project_service.list()
+ print("Process %s projects" % len(projects))
- # Loop through list of tenants and clean them up.
- for tenant in tenants:
- self._add_admin(tenant['id'])
- self._clean_tenant(tenant)
+ # Loop through list of projects and clean them up.
+ for project in projects:
+ self._add_admin(project['id'])
+ self._clean_project(project)
kwargs = {'data': self.dry_run_data,
'is_dry_run': is_dry_run,
@@ -169,49 +172,51 @@
self._remove_admin_user_roles()
def _remove_admin_user_roles(self):
- tenant_ids = self.admin_role_added
- LOG.debug("Removing admin user roles where needed for tenants: %s",
- tenant_ids)
- for tenant_id in tenant_ids:
- self._remove_admin_role(tenant_id)
+ project_ids = self.admin_role_added
+ LOG.debug("Removing admin user roles where needed for projects: %s",
+ project_ids)
+ for project_id in project_ids:
+ self._remove_admin_role(project_id)
- def _clean_tenant(self, tenant):
- print("Cleaning tenant: %s " % tenant['name'])
+ def _clean_project(self, project):
+ print("Cleaning project: %s " % project['name'])
is_dry_run = self.options.dry_run
dry_run_data = self.dry_run_data
is_preserve = not self.options.delete_tempest_conf_objects
- tenant_id = tenant['id']
- tenant_name = tenant['name']
- tenant_data = None
+ project_id = project['id']
+ project_name = project['name']
+ project_data = None
if is_dry_run:
- tenant_data = dry_run_data["_tenants_to_clean"][tenant_id] = {}
- tenant_data['name'] = tenant_name
+ project_data = dry_run_data["_projects_to_clean"][project_id] = {}
+ project_data['name'] = project_name
kwargs = {"username": CONF.auth.admin_username,
"password": CONF.auth.admin_password,
- "tenant_name": tenant['name']}
+ "project_name": project['name']}
mgr = clients.Manager(credentials=credentials.get_credentials(
**kwargs))
- kwargs = {'data': tenant_data,
+ kwargs = {'data': project_data,
'is_dry_run': is_dry_run,
'saved_state_json': None,
'is_preserve': is_preserve,
'is_save_state': False,
- 'tenant_id': tenant_id}
- for service in self.tenant_services:
+ 'project_id': project_id}
+ for service in self.project_services:
svc = service(mgr, **kwargs)
svc.run()
def _init_admin_ids(self):
- tn_cl = self.admin_mgr.tenants_client
- rl_cl = self.admin_mgr.roles_client
+ pr_cl = self.admin_mgr.projects_client
+ rl_cl = self.admin_mgr.roles_v3_client
+ rla_cl = self.admin_mgr.role_assignments_client
+ us_cl = self.admin_mgr.users_v3_client
- tenant = identity.get_tenant_by_name(tn_cl,
- CONF.auth.admin_project_name)
- self.admin_tenant_id = tenant['id']
-
- user = identity.get_user_by_username(tn_cl, self.admin_tenant_id,
- CONF.auth.admin_username)
+ project = identity.get_project_by_name(pr_cl,
+ CONF.auth.admin_project_name)
+ self.admin_project_id = project['id']
+ user = identity.get_user_by_project(us_cl, rla_cl,
+ self.admin_project_id,
+ CONF.auth.admin_username)
self.admin_id = user['id']
roles = rl_cl.list_roles()['roles']
@@ -236,7 +241,7 @@
dest='delete_tempest_conf_objects',
default=False,
help="Force deletion of the tempest and "
- "alternate tempest users and tenants.")
+ "alternate tempest users and projects.")
parser.add_argument('--dry-run', action="store_true",
dest='dry_run', default=False,
help="Generate JSON file:" + DRY_RUN_JSON +
@@ -247,44 +252,44 @@
def get_description(self):
return 'Cleanup after tempest run'
- def _add_admin(self, tenant_id):
- rl_cl = self.admin_mgr.roles_client
+ def _add_admin(self, project_id):
+ rl_cl = self.admin_mgr.roles_v3_client
needs_role = True
- roles = rl_cl.list_user_roles_on_project(tenant_id,
+ roles = rl_cl.list_user_roles_on_project(project_id,
self.admin_id)['roles']
for role in roles:
if role['id'] == self.admin_role_id:
needs_role = False
- LOG.debug("User already had admin privilege for this tenant")
+ LOG.debug("User already had admin privilege for this project")
if needs_role:
- LOG.debug("Adding admin privilege for : %s", tenant_id)
- rl_cl.create_user_role_on_project(tenant_id, self.admin_id,
+ LOG.debug("Adding admin privilege for : %s", project_id)
+ rl_cl.create_user_role_on_project(project_id, self.admin_id,
self.admin_role_id)
- self.admin_role_added.append(tenant_id)
+ self.admin_role_added.append(project_id)
- def _remove_admin_role(self, tenant_id):
- LOG.debug("Remove admin user role for tenant: %s", tenant_id)
+ def _remove_admin_role(self, project_id):
+ LOG.debug("Remove admin user role for projectt: %s", project_id)
# Must initialize Admin Manager for each user role
# Otherwise authentication exception is thrown, weird
id_cl = clients.Manager(
credentials.get_configured_admin_credentials()).identity_client
- if (self._tenant_exists(tenant_id)):
+ if (self._project_exists(project_id)):
try:
- id_cl.delete_role_from_user_on_project(tenant_id,
+ id_cl.delete_role_from_user_on_project(project_id,
self.admin_id,
self.admin_role_id)
except Exception as ex:
- LOG.exception("Failed removing role from tenant which still"
+ LOG.exception("Failed removing role from project which still"
"exists, exception: %s", ex)
- def _tenant_exists(self, tenant_id):
- tn_cl = self.admin_mgr.tenants_client
+ def _project_exists(self, project_id):
+ pr_cl = self.admin_mgr.projects_client
try:
- t = tn_cl.show_tenant(tenant_id)
- LOG.debug("Tenant is: %s", str(t))
+ p = pr_cl.show_project(project_id)
+ LOG.debug("Project is: %s", str(p))
return True
except Exception as ex:
- LOG.debug("Tenant no longer exists? %s", ex)
+ LOG.debug("Project no longer exists? %s", ex)
return False
def _init_state(self):
diff --git a/tempest/cmd/cleanup_service.py b/tempest/cmd/cleanup_service.py
index c75bc85..d1e80f1 100644
--- a/tempest/cmd/cleanup_service.py
+++ b/tempest/cmd/cleanup_service.py
@@ -32,7 +32,7 @@
CONF_PRIV_NETWORK_NAME = None
CONF_PUB_NETWORK = None
CONF_PUB_ROUTER = None
-CONF_TENANTS = None
+CONF_PROJECTS = None
CONF_USERS = None
IS_CINDER = None
@@ -50,7 +50,7 @@
global CONF_PRIV_NETWORK_NAME
global CONF_PUB_NETWORK
global CONF_PUB_ROUTER
- global CONF_TENANTS
+ global CONF_PROJECTS
global CONF_USERS
global IS_CINDER
global IS_GLANCE
@@ -69,7 +69,7 @@
CONF_PRIV_NETWORK_NAME = CONF.compute.fixed_network_name
CONF_PUB_NETWORK = CONF.network.public_network_id
CONF_PUB_ROUTER = CONF.network.public_router_id
- CONF_TENANTS = [CONF.auth.admin_project_name]
+ CONF_PROJECTS = [CONF.auth.admin_project_name]
CONF_USERS = [CONF.auth.admin_username]
if IS_NEUTRON:
@@ -82,14 +82,14 @@
am = clients.Manager(
credentials.get_configured_admin_credentials())
net_cl = am.networks_client
- tn_cl = am.tenants_client
+ pr_cl = am.projects_client
networks = net_cl.list_networks()
- tenant = identity.get_tenant_by_name(tn_cl, project_name)
- t_id = tenant['id']
+ project = identity.get_project_by_name(pr_cl, project_name)
+ p_id = project['id']
n_id = None
for net in networks['networks']:
- if (net['tenant_id'] == t_id and net['name'] == net_name):
+ if (net['project_id'] == p_id and net['name'] == net_name):
n_id = net['id']
break
return n_id
@@ -141,7 +141,7 @@
def __init__(self, manager, **kwargs):
super(SnapshotService, self).__init__(kwargs)
- self.client = manager.snapshots_client
+ self.client = manager.snapshots_client_latest
def list(self):
client = self.client
@@ -319,7 +319,7 @@
class VolumeService(BaseService):
def __init__(self, manager, **kwargs):
super(VolumeService, self).__init__(kwargs)
- self.client = manager.volumes_client
+ self.client = manager.volumes_client_latest
def list(self):
client = self.client
@@ -344,7 +344,7 @@
class VolumeQuotaService(BaseService):
def __init__(self, manager, **kwargs):
super(VolumeQuotaService, self).__init__(kwargs)
- self.client = manager.volume_quotas_client
+ self.client = manager.volume_quotas_v2_client
def delete(self):
client = self.client
@@ -786,14 +786,14 @@
class IdentityService(BaseService):
def __init__(self, manager, **kwargs):
super(IdentityService, self).__init__(kwargs)
- self.client = manager.identity_client
+ self.client = manager.identity_v3_client
class UserService(BaseService):
def __init__(self, manager, **kwargs):
super(UserService, self).__init__(kwargs)
- self.client = manager.users_client
+ self.client = manager.users_v3_client
def list(self):
users = self.client.list_users()['users']
@@ -872,43 +872,43 @@
self.data['roles'][role['id']] = role['name']
-class TenantService(BaseService):
+class ProjectService(BaseService):
def __init__(self, manager, **kwargs):
- super(TenantService, self).__init__(kwargs)
- self.client = manager.tenants_client
+ super(ProjectService, self).__init__(kwargs)
+ self.client = manager.projects_client
def list(self):
- tenants = self.client.list_tenants()['tenants']
+ projects = self.client.list_projects()['projects']
if not self.is_save_state:
- tenants = [tenant for tenant in tenants if (tenant['id']
- not in self.saved_state_json['tenants'].keys()
- and tenant['name'] != CONF.auth.admin_project_name)]
+ projects = [project for project in projects if (project['id']
+ not in self.saved_state_json['projects'].keys()
+ and project['name'] != CONF.auth.admin_project_name)]
if self.is_preserve:
- tenants = [tenant for tenant in tenants if tenant['name']
- not in CONF_TENANTS]
+ projects = [project for project in projects if project['name']
+ not in CONF_PROJECTS]
- LOG.debug("List count, %s Tenants after reconcile", len(tenants))
- return tenants
+ LOG.debug("List count, %s Projects after reconcile", len(projects))
+ return projects
def delete(self):
- tenants = self.list()
- for tenant in tenants:
+ projects = self.list()
+ for project in projects:
try:
- self.client.delete_tenant(tenant['id'])
+ self.client.delete_project(project['id'])
except Exception:
- LOG.exception("Delete Tenant exception.")
+ LOG.exception("Delete project exception.")
def dry_run(self):
- tenants = self.list()
- self.data['tenants'] = tenants
+ projects = self.list()
+ self.data['projects'] = projects
def save_state(self):
- tenants = self.list()
- self.data['tenants'] = {}
- for tenant in tenants:
- self.data['tenants'][tenant['id']] = tenant['name']
+ projects = self.list()
+ self.data['projects'] = {}
+ for project in projects:
+ self.data['projects'][project['id']] = project['name']
class DomainService(BaseService):
@@ -948,35 +948,35 @@
self.data['domains'][domain['id']] = domain['name']
-def get_tenant_cleanup_services():
- tenant_services = []
+def get_project_cleanup_services():
+ project_services = []
# TODO(gmann): Tempest should provide some plugin hook for cleanup
# script extension to plugin tests also.
if IS_NOVA:
- tenant_services.append(ServerService)
- tenant_services.append(KeyPairService)
- tenant_services.append(SecurityGroupService)
- tenant_services.append(ServerGroupService)
+ project_services.append(ServerService)
+ project_services.append(KeyPairService)
+ project_services.append(SecurityGroupService)
+ project_services.append(ServerGroupService)
if not IS_NEUTRON:
- tenant_services.append(FloatingIpService)
- tenant_services.append(NovaQuotaService)
+ project_services.append(FloatingIpService)
+ project_services.append(NovaQuotaService)
if IS_HEAT:
- tenant_services.append(StackService)
+ project_services.append(StackService)
if IS_NEUTRON:
- tenant_services.append(NetworkFloatingIpService)
+ project_services.append(NetworkFloatingIpService)
if utils.is_extension_enabled('metering', 'network'):
- tenant_services.append(NetworkMeteringLabelRuleService)
- tenant_services.append(NetworkMeteringLabelService)
- tenant_services.append(NetworkRouterService)
- tenant_services.append(NetworkPortService)
- tenant_services.append(NetworkSubnetService)
- tenant_services.append(NetworkService)
- tenant_services.append(NetworkSecGroupService)
+ project_services.append(NetworkMeteringLabelRuleService)
+ project_services.append(NetworkMeteringLabelService)
+ project_services.append(NetworkRouterService)
+ project_services.append(NetworkPortService)
+ project_services.append(NetworkSubnetService)
+ project_services.append(NetworkService)
+ project_services.append(NetworkSecGroupService)
if IS_CINDER:
- tenant_services.append(SnapshotService)
- tenant_services.append(VolumeService)
- tenant_services.append(VolumeQuotaService)
- return tenant_services
+ project_services.append(SnapshotService)
+ project_services.append(VolumeService)
+ project_services.append(VolumeQuotaService)
+ return project_services
def get_global_cleanup_services():
@@ -986,7 +986,7 @@
if IS_GLANCE:
global_services.append(ImageService)
global_services.append(UserService)
- global_services.append(TenantService)
+ global_services.append(ProjectService)
global_services.append(DomainService)
global_services.append(RoleService)
return global_services
diff --git a/tempest/cmd/run.py b/tempest/cmd/run.py
index e71032a..0d847bd 100644
--- a/tempest/cmd/run.py
+++ b/tempest/cmd/run.py
@@ -19,11 +19,11 @@
==============
Tempest run has several options:
- * **--regex/-r**: This is a selection regex like what testr uses. It will run
- any tests that match on re.match() with the regex
- * **--smoke/-s**: Run all the tests tagged as smoke
+* ``--regex, -r``: This is a selection regex like what testr uses. It will run
+ any tests that match on re.match() with the regex
+* ``--smoke, -s``: Run all the tests tagged as smoke
-There are also the **--blacklist-file** and **--whitelist-file** options that
+There are also the ``--blacklist-file`` and ``--whitelist-file`` options that
let you pass a filepath to tempest run with the file format being a line
separated regex, with '#' used to signify the start of a comment on a line.
For example::
@@ -44,15 +44,21 @@
When combined with a whitelist file all the regexes from the file and the CLI
regexes will be ORed.
-You can also use the **--list-tests** option in conjunction with selection
+You can also use the ``--list-tests`` option in conjunction with selection
arguments to list which tests will be run.
+You can also use the ``--load-list`` option that lets you pass a filepath to
+tempest run with the file format being in a non-regex format, similar to the
+tests generated by the ``--list-tests`` option. You can specify target tests
+by removing unnecessary tests from a list file which is generated from
+``--list-tests`` option.
+
Test Execution
==============
There are several options to control how the tests are executed. By default
tempest will run in parallel with a worker for each CPU present on the machine.
-If you want to adjust the number of workers use the **--concurrency** option
-and if you want to run tests serially use **--serial/-t**
+If you want to adjust the number of workers use the ``--concurrency`` option
+and if you want to run tests serially use ``--serial/-t``
Running with Workspaces
-----------------------
@@ -76,7 +82,7 @@
===========
By default tempest run's output to STDOUT will be generated using the
subunit-trace output filter. But, if you would prefer a subunit v2 stream be
-output to STDOUT use the **--subunit** flag
+output to STDOUT use the ``--subunit`` flag
Combining Runs
==============
@@ -84,7 +90,7 @@
There are certain situations in which you want to split a single run of tempest
across 2 executions of tempest run. (for example to run part of the tests
serially and others in parallel) To accomplish this but still treat the results
-as a single run you can leverage the **--combine** option which will append
+as a single run you can leverage the ``--combine`` option which will append
the current run's results with the previous runs.
"""
@@ -267,6 +273,12 @@
help='Path to a blacklist file, this file '
'contains a separate regex exclude on '
'each newline')
+ list_selector.add_argument('--load-list', '--load_list',
+ help='Path to a non-regex whitelist file, '
+ 'this file contains a seperate test '
+ 'on each newline. This command'
+ 'supports files created by the tempest'
+ 'run ``--list-tests`` command')
# list only args
parser.add_argument('--list-tests', '-l', action='store_true',
help='List tests',
@@ -318,6 +330,8 @@
options.append("--parallel")
if parsed_args.concurrency:
options.append("--concurrency=%s" % parsed_args.concurrency)
+ if parsed_args.load_list:
+ options.append("--load-list=%s" % parsed_args.load_list)
return options
def _run(self, regex, options):
diff --git a/tempest/cmd/subunit_describe_calls.py b/tempest/cmd/subunit_describe_calls.py
index f9ebe20..f0ade7e 100644
--- a/tempest/cmd/subunit_describe_calls.py
+++ b/tempest/cmd/subunit_describe_calls.py
@@ -21,17 +21,14 @@
Runtime Arguments
-----------------
-**--subunit, -s**: (Optional) The path to the subunit file being parsed,
-defaults to stdin
-
-**--non-subunit-name, -n**: (Optional) The file_name that the logs are being
-stored in
-
-**--output-file, -o**: (Optional) The path where the JSON output will be
-written to. This contains more information than is present in stdout.
-
-**--ports, -p**: (Optional) The path to a JSON file describing the ports being
-used by different services
+* ``--subunit, -s``: (Optional) The path to the subunit file being parsed,
+ defaults to stdin
+* ``--non-subunit-name, -n``: (Optional) The file_name that the logs are being
+ stored in
+* ``--output-file, -o``: (Optional) The path where the JSON output will be
+ written to. This contains more information than is present in stdout.
+* ``--ports, -p``: (Optional) The path to a JSON file describing the ports
+ being used by different services
Usage
-----
diff --git a/tempest/cmd/verify_tempest_config.py b/tempest/cmd/verify_tempest_config.py
index a72493d..fdf28d5 100644
--- a/tempest/cmd/verify_tempest_config.py
+++ b/tempest/cmd/verify_tempest_config.py
@@ -76,7 +76,6 @@
from tempest import config
import tempest.lib.common.http
from tempest.lib import exceptions as lib_exc
-from tempest.services import object_storage
CONF = config.CONF
@@ -197,10 +196,6 @@
def verify_keystone_api_versions(os, update):
# Check keystone api versions
versions = _get_api_versions(os, 'keystone')
- if (CONF.identity_feature_enabled.api_v2 !=
- contains_version('v2.', versions)):
- print_and_or_update('api_v2', 'identity-feature-enabled',
- not CONF.identity_feature_enabled.api_v2, update)
if (CONF.identity_feature_enabled.api_v3 !=
contains_version('v3.', versions)):
print_and_or_update('api_v3', 'identity-feature-enabled',
@@ -236,11 +231,10 @@
def get_extension_client(os, service):
- params = config.service_client_config('object-storage')
extensions_client = {
'nova': os.compute.ExtensionsClient(),
'neutron': os.network.ExtensionsClient(),
- 'swift': object_storage.CapabilitiesClient(os.auth_provider, **params),
+ 'swift': os.object_storage.CapabilitiesClient(),
# NOTE: Cinder v3 API is current and v2 and v1 are deprecated.
# V3 extension API is the same as v2, so we reuse the v2 client
# for v3 API also.
diff --git a/tempest/cmd/workspace.py b/tempest/cmd/workspace.py
index 8166b4f..929a584 100644
--- a/tempest/cmd/workspace.py
+++ b/tempest/cmd/workspace.py
@@ -26,28 +26,28 @@
register
--------
-Registers a new tempest workspace via a given --name and --path
+Registers a new tempest workspace via a given ``--name`` and ``--path``
rename
------
-Renames a tempest workspace from --old-name to --new-name
+Renames a tempest workspace from ``--old-name`` to ``--new-name``
move
----
-Changes the path of a given tempest workspace --name to --path
+Changes the path of a given tempest workspace ``--name`` to ``--path``
remove
------
-Deletes the entry for a given tempest workspace --name
+Deletes the entry for a given tempest workspace ``--name``
---rmdir Deletes the given tempest workspace directory
+``--rmdir`` Deletes the given tempest workspace directory
General Options
===============
- **--workspace_path**: Allows the user to specify a different location for the
- workspace.yaml file containing the workspace definitions
- instead of ~/.tempest/workspace.yaml
+* ``--workspace_path``: Allows the user to specify a different location for the
+ workspace.yaml file containing the workspace definitions instead of
+ ``~/.tempest/workspace.yaml``
"""
import os
diff --git a/tempest/common/compute.py b/tempest/common/compute.py
index aa50a7c..f0fceb3 100644
--- a/tempest/common/compute.py
+++ b/tempest/common/compute.py
@@ -128,6 +128,8 @@
"this stage.")
raise ValueError(msg)
+ LOG.debug("Provisioning test server with validation resources %s",
+ validation_resources)
if 'security_groups' in kwargs:
kwargs['security_groups'].append(
{'name': validation_resources['security_group']['name']})
diff --git a/tempest/common/credentials_factory.py b/tempest/common/credentials_factory.py
index a340531..da34975 100644
--- a/tempest/common/credentials_factory.py
+++ b/tempest/common/credentials_factory.py
@@ -219,13 +219,6 @@
'alt_user': ('identity', 'alt')
}
-DEFAULT_PARAMS = {
- 'disable_ssl_certificate_validation':
- CONF.identity.disable_ssl_certificate_validation,
- 'ca_certs': CONF.identity.ca_certificates_file,
- 'trace_requests': CONF.debug.trace_requests
-}
-
def get_configured_admin_credentials(fill_in=True, identity_version=None):
"""Get admin credentials from the config file
@@ -252,7 +245,7 @@
if identity_version == 'v3':
conf_attributes.append('domain_name')
# Read the parts of credentials from config
- params = DEFAULT_PARAMS.copy()
+ params = config.service_client_config()
for attr in conf_attributes:
params[attr] = getattr(CONF.auth, 'admin_' + attr)
# Build and validate credentials. We are reading configured credentials,
@@ -282,7 +275,7 @@
:param kwargs: Attributes to be used to build the Credentials object.
:returns: An object of a sub-type of `auth.Credentials`
"""
- params = dict(DEFAULT_PARAMS, **kwargs)
+ params = dict(config.service_client_config(), **kwargs)
identity_version = identity_version or CONF.identity.auth_version
# In case of "v3" add the domain from config if not specified
# To honour the "default_credentials_domain_name", if not domain
diff --git a/tempest/common/identity.py b/tempest/common/identity.py
index 469defe..eaf651b 100644
--- a/tempest/common/identity.py
+++ b/tempest/common/identity.py
@@ -13,8 +13,21 @@
# License for the specific language governing permissions and limitations
# under the License.
+from tempest import config
+from tempest.lib.common import cred_client
from tempest.lib import exceptions as lib_exc
+CONF = config.CONF
+
+
+def get_project_by_name(client, project_name):
+ projects = client.list_projects({'name': project_name})['projects']
+ for project in projects:
+ if project['name'] == project_name:
+ return project
+ raise lib_exc.NotFound('No such project(%s) in %s' % (project_name,
+ projects))
+
def get_tenant_by_name(client, tenant_name):
tenants = client.list_tenants()['tenants']
@@ -30,3 +43,49 @@
if user['name'] == username:
return user
raise lib_exc.NotFound('No such user(%s) in %s' % (username, users))
+
+
+def get_user_by_project(users_client, roles_client, project_id, username):
+ users = users_client.list_users(**{'name': username})['users']
+ users_in_project = roles_client.list_role_assignments(
+ **{'scope.project.id': project_id})['role_assignments']
+ for user in users:
+ if user['name'] == username:
+ for u in users_in_project:
+ if u['user']['id'] == user['id']:
+ return user
+ raise lib_exc.NotFound('No such user(%s) in %s' % (username, users))
+
+
+def identity_utils(clients):
+ """A client that abstracts v2 and v3 identity operations.
+
+ This can be used for creating and tearing down projects in tests. It
+ should not be used for testing identity features.
+
+ :param clients: a client manager.
+ :return
+ """
+ if CONF.identity.auth_version == 'v2':
+ client = clients.identity_client
+ users_client = clients.users_client
+ project_client = clients.tenants_client
+ roles_client = clients.roles_client
+ domains_client = None
+ else:
+ client = clients.identity_v3_client
+ users_client = clients.users_v3_client
+ project_client = clients.projects_client
+ roles_client = clients.roles_v3_client
+ domains_client = clients.domains_client
+
+ try:
+ domain = client.auth_provider.credentials.project_domain_name
+ except AttributeError:
+ domain = CONF.auth.default_credentials_domain_name
+
+ return cred_client.get_creds_client(client, project_client,
+ users_client,
+ roles_client,
+ domains_client,
+ project_domain_name=domain)
diff --git a/tempest/common/utils/__init__.py b/tempest/common/utils/__init__.py
index 5a86caa..aa81864 100644
--- a/tempest/common/utils/__init__.py
+++ b/tempest/common/utils/__init__.py
@@ -78,7 +78,7 @@
decorators.attr(type=list(args))(f)
@functools.wraps(f)
- def wrapper(self, *func_args, **func_kwargs):
+ def wrapper(*func_args, **func_kwargs):
service_list = get_service_list()
for service in args:
@@ -86,7 +86,7 @@
msg = 'Skipped because the %s service is not available' % (
service)
raise testtools.TestCase.skipException(msg)
- return f(self, *func_args, **func_kwargs)
+ return f(*func_args, **func_kwargs)
return wrapper
return decorator
diff --git a/tempest/common/waiters.py b/tempest/common/waiters.py
index 10afee0..08e2a12 100644
--- a/tempest/common/waiters.py
+++ b/tempest/common/waiters.py
@@ -179,15 +179,13 @@
raise lib_exc.TimeoutException(message)
-def wait_for_volume_resource_status(client, resource_id, statuses):
- """Waits for a volume resource to reach any of the specified statuses.
+def wait_for_volume_resource_status(client, resource_id, status):
+ """Waits for a volume resource to reach a given status.
This function is a common function for volume, snapshot and backup
resources. The function extracts the name of the desired resource from
the client class name of the resource.
"""
- if not isinstance(statuses, list):
- statuses = [statuses]
resource_name = re.findall(
r'(volume|group-snapshot|snapshot|backup|group)',
client.resource_type)[-1].replace('-', '_')
@@ -195,11 +193,11 @@
resource_status = show_resource(resource_id)[resource_name]['status']
start = int(time.time())
- while resource_status not in statuses:
+ while resource_status != status:
time.sleep(client.build_interval)
resource_status = show_resource(resource_id)[
'{}'.format(resource_name)]['status']
- if resource_status == 'error' and resource_status not in statuses:
+ if resource_status == 'error' and resource_status != status:
raise exceptions.VolumeResourceBuildErrorException(
resource_name=resource_name, resource_id=resource_id)
if resource_name == 'volume' and resource_status == 'error_restoring':
@@ -208,11 +206,11 @@
if int(time.time()) - start >= client.build_timeout:
message = ('%s %s failed to reach %s status (current %s) '
'within the required time (%s s).' %
- (resource_name, resource_id, statuses, resource_status,
+ (resource_name, resource_id, status, resource_status,
client.build_timeout))
raise lib_exc.TimeoutException(message)
LOG.info('%s %s reached %s after waiting for %f seconds',
- resource_name, resource_id, statuses, time.time() - start)
+ resource_name, resource_id, status, time.time() - start)
def wait_for_volume_retype(client, volume_id, new_volume_type):
diff --git a/tempest/config.py b/tempest/config.py
index e78a07f..5eee86d 100644
--- a/tempest/config.py
+++ b/tempest/config.py
@@ -15,15 +15,12 @@
from __future__ import print_function
-import functools
import os
import tempfile
-import debtcollector.removals
from oslo_concurrency import lockutils
from oslo_config import cfg
from oslo_log import log as logging
-import testtools
from tempest.lib import exceptions
from tempest.lib.services import clients
@@ -197,6 +194,8 @@
default=60,
help='Timeout in seconds to wait for the http request to '
'return'),
+ cfg.StrOpt('proxy_url',
+ help='Specify an http proxy to use.')
]
identity_feature_group = cfg.OptGroup(name='identity-feature-enabled',
@@ -208,8 +207,14 @@
help='Does the identity service have delegation and '
'impersonation enabled'),
cfg.BoolOpt('api_v2',
- default=True,
- help='Is the v2 identity API enabled'),
+ default=False,
+ help='Is the v2 identity API enabled',
+ deprecated_for_removal=True,
+ deprecated_reason='The identity v2.0 API was removed in the '
+ 'Queens release. Tests that exercise the '
+ 'v2.0 API will be removed from tempest in '
+ 'the v22.0.0 release. They are kept only to '
+ 'test stable branches.'),
cfg.BoolOpt('api_v2_admin',
default=True,
help="Is the v2 identity admin API available? This setting "
@@ -222,7 +227,8 @@
help="A list of enabled identity extensions with a special "
"entry all which indicates every extension is enabled. "
"Empty list indicates all extensions are disabled. "
- "To get the list of extensions run: 'keystone discover'"),
+ "To get the list of extensions run: "
+ "'openstack extension list --identity'"),
# TODO(rodrigods): This is a feature flag for bug 1590578 which is fixed
# in Newton and Ocata. This option can be removed after Mitaka is end of
# life.
@@ -306,9 +312,9 @@
default=0,
help='Time in seconds before a shelved instance is eligible '
'for removing from a host. -1 never offload, 0 offload '
- 'when shelved. This time should be the same as the time '
- 'of nova.conf, and some tests will run for as long as the '
- 'time.'),
+ 'when shelved. This configuration value should be same as '
+ '[nova.DEFAULT]->shelved_offload_time in nova.conf, and '
+ 'some tests will run for as long as the time.'),
cfg.IntOpt('min_compute_nodes',
default=1,
help=('The minimum number of compute nodes expected. This will '
@@ -839,7 +845,14 @@
help="Is the v2 volume API enabled"),
cfg.BoolOpt('api_v3',
default=True,
- help="Is the v3 volume API enabled")
+ help="Is the v3 volume API enabled"),
+ cfg.BoolOpt('extend_attached_volume',
+ default=False,
+ help='Does the cloud support extending the size of a volume '
+ 'which is currently attached to a server instance? This '
+ 'depends on the 3.42 volume API microversion and the '
+ '2.51 compute API microversion. Also, not all volume or '
+ 'compute backends support this operation.')
]
@@ -1284,79 +1297,6 @@
CONF = TempestConfigProxy()
-@debtcollector.removals.remove(
- message='use testtools.skipUnless instead', removal_version='Queens')
-def skip_unless_config(*args):
- """Decorator to raise a skip if a config opt doesn't exist or is False
-
- :param str group: The first arg, the option group to check
- :param str name: The second arg, the option name to check
- :param str msg: Optional third arg, the skip msg to use if a skip is raised
- :raises testtools.TestCaseskipException: If the specified config option
- doesn't exist or it exists and evaluates to False
- """
- def decorator(f):
- group = args[0]
- name = args[1]
-
- @functools.wraps(f)
- def wrapper(self, *func_args, **func_kwargs):
- if not hasattr(CONF, group):
- msg = "Config group %s doesn't exist" % group
- raise testtools.TestCase.skipException(msg)
-
- conf_group = getattr(CONF, group)
- if not hasattr(conf_group, name):
- msg = "Config option %s.%s doesn't exist" % (group,
- name)
- raise testtools.TestCase.skipException(msg)
-
- value = getattr(conf_group, name)
- if not value:
- if len(args) == 3:
- msg = args[2]
- else:
- msg = "Config option %s.%s is false" % (group,
- name)
- raise testtools.TestCase.skipException(msg)
- return f(self, *func_args, **func_kwargs)
- return wrapper
- return decorator
-
-
-@debtcollector.removals.remove(
- message='use testtools.skipIf instead', removal_version='Queens')
-def skip_if_config(*args):
- """Raise a skipException if a config exists and is True
-
- :param str group: The first arg, the option group to check
- :param str name: The second arg, the option name to check
- :param str msg: Optional third arg, the skip msg to use if a skip is raised
- :raises testtools.TestCase.skipException: If the specified config option
- exists and evaluates to True
- """
- def decorator(f):
- group = args[0]
- name = args[1]
-
- @functools.wraps(f)
- def wrapper(self, *func_args, **func_kwargs):
- if hasattr(CONF, group):
- conf_group = getattr(CONF, group)
- if hasattr(conf_group, name):
- value = getattr(conf_group, name)
- if value:
- if len(args) == 3:
- msg = args[2]
- else:
- msg = "Config option %s.%s is false" % (group,
- name)
- raise testtools.TestCase.skipException(msg)
- return f(self, *func_args, **func_kwargs)
- return wrapper
- return decorator
-
-
def service_client_config(service_client_name=None):
"""Return a dict with the parameters to init service clients
@@ -1377,6 +1317,7 @@
* `ca_certs`
* `trace_requests`
* `http_timeout`
+ * `proxy_url`
The dict returned by this does not fit a few service clients:
@@ -1399,7 +1340,8 @@
CONF.identity.disable_ssl_certificate_validation,
'ca_certs': CONF.identity.ca_certificates_file,
'trace_requests': CONF.debug.trace_requests,
- 'http_timeout': CONF.service_clients.http_timeout
+ 'http_timeout': CONF.service_clients.http_timeout,
+ 'proxy_url': CONF.service_clients.proxy_url,
}
if service_client_name is None:
@@ -1453,7 +1395,7 @@
module = service_clients[service_client]
configs = service_client.split('.')[0]
service_client_data = dict(
- name=service_client.replace('.', '_'),
+ name=service_client.replace('.', '_').replace('-', '_'),
service_version=service_client,
module_path=module.__name__,
client_names=module.__all__,
diff --git a/tempest/exceptions.py b/tempest/exceptions.py
index a8a6ff0..a430d5d 100644
--- a/tempest/exceptions.py
+++ b/tempest/exceptions.py
@@ -52,9 +52,5 @@
"the configured network")
-class RFCViolation(exceptions.RestClientException):
- message = "RFC Violation"
-
-
class InvalidServiceTag(exceptions.TempestException):
message = "Invalid service tag"
diff --git a/tempest/lib/auth.py b/tempest/lib/auth.py
index ab4308f..2dd9d00 100644
--- a/tempest/lib/auth.py
+++ b/tempest/lib/auth.py
@@ -261,12 +261,13 @@
def __init__(self, credentials, auth_url,
disable_ssl_certificate_validation=None,
ca_certs=None, trace_requests=None, scope='project',
- http_timeout=None):
+ http_timeout=None, proxy_url=None):
super(KeystoneAuthProvider, self).__init__(credentials, scope)
self.dscv = disable_ssl_certificate_validation
self.ca_certs = ca_certs
self.trace_requests = trace_requests
self.http_timeout = http_timeout
+ self.proxy_url = proxy_url
self.auth_url = auth_url
self.auth_client = self._auth_client(auth_url)
@@ -345,7 +346,7 @@
return json_v2id.TokenClient(
auth_url, disable_ssl_certificate_validation=self.dscv,
ca_certs=self.ca_certs, trace_requests=self.trace_requests,
- http_timeout=self.http_timeout)
+ http_timeout=self.http_timeout, proxy_url=self.proxy_url)
def _auth_params(self):
"""Auth parameters to be passed to the token request
@@ -433,7 +434,7 @@
return json_v3id.V3TokenClient(
auth_url, disable_ssl_certificate_validation=self.dscv,
ca_certs=self.ca_certs, trace_requests=self.trace_requests,
- http_timeout=self.http_timeout)
+ http_timeout=self.http_timeout, proxy_url=self.proxy_url)
def _auth_params(self):
"""Auth parameters to be passed to the token request
@@ -599,7 +600,8 @@
def get_credentials(auth_url, fill_in=True, identity_version='v2',
disable_ssl_certificate_validation=None, ca_certs=None,
- trace_requests=None, http_timeout=None, **kwargs):
+ trace_requests=None, http_timeout=None, proxy_url=None,
+ **kwargs):
"""Builds a credentials object based on the configured auth_version
:param auth_url (string): Full URI of the OpenStack Identity API(Keystone)
@@ -617,6 +619,7 @@
:param trace_requests: trace in log API requests to the auth system
:param http_timeout: timeout in seconds to wait for the http request to
return
+ :param proxy_url: URL of HTTP(s) proxy used when fill_in is True
:param kwargs (dict): Dict of credential key/value pairs
Examples:
@@ -641,7 +644,7 @@
auth_provider = auth_provider_class(
creds, auth_url, disable_ssl_certificate_validation=dscv,
ca_certs=ca_certs, trace_requests=trace_requests,
- http_timeout=http_timeout)
+ http_timeout=http_timeout, proxy_url=proxy_url)
creds = auth_provider.fill_credentials()
return creds
diff --git a/tempest/lib/cli/base.py b/tempest/lib/cli/base.py
index 5468a7b..f39ecbc 100644
--- a/tempest/lib/cli/base.py
+++ b/tempest/lib/cli/base.py
@@ -93,10 +93,20 @@
:type insecure: boolean
:param prefix: prefix to insert before commands
:type prefix: string
+ :param user_domain_name: User's domain name
+ :type user_domain_name: string
+ :param user_domain_id: User's domain ID
+ :type user_domain_id: string
+ :param project_domain_name: Project's domain name
+ :type project_domain_name: string
+ :param project_domain_id: Project's domain ID
+ :type project_domain_id: string
"""
def __init__(self, username='', password='', tenant_name='', uri='',
- cli_dir='', insecure=False, prefix='', *args, **kwargs):
+ cli_dir='', insecure=False, prefix='', user_domain_name=None,
+ user_domain_id=None, project_domain_name=None,
+ project_domain_id=None, *args, **kwargs):
"""Initialize a new CLIClient object."""
super(CLIClient, self).__init__()
self.cli_dir = cli_dir if cli_dir else '/usr/bin'
@@ -106,6 +116,10 @@
self.uri = uri
self.insecure = insecure
self.prefix = prefix
+ self.user_domain_name = user_domain_name
+ self.user_domain_id = user_domain_id
+ self.project_domain_name = project_domain_name
+ self.project_domain_id = project_domain_id
def nova(self, action, flags='', params='', fail_ok=False,
endpoint_type='publicURL', merge_stderr=False):
@@ -366,6 +380,14 @@
self.tenant_name,
self.password,
self.uri))
+ if self.user_domain_name is not None:
+ creds += ' --os-user-domain-name %s' % self.user_domain_name
+ if self.user_domain_id is not None:
+ creds += ' --os-user-domain-id %s' % self.user_domain_id
+ if self.project_domain_name is not None:
+ creds += ' --os-project-domain-name %s' % self.project_domain_name
+ if self.project_domain_id is not None:
+ creds += ' --os-project-domain-id %s' % self.project_domain_id
if self.insecure:
flags = creds + ' --insecure ' + flags
else:
diff --git a/tempest/lib/common/api_version_utils.py b/tempest/lib/common/api_version_utils.py
index 98f174d..bcb076b 100644
--- a/tempest/lib/common/api_version_utils.py
+++ b/tempest/lib/common/api_version_utils.py
@@ -12,7 +12,6 @@
# License for the specific language governing permissions and limitations
# under the License.
-from oslo_log import log as logging
import testtools
from tempest.lib.common import api_version_request
@@ -20,7 +19,6 @@
LATEST_MICROVERSION = 'latest'
-LOG = logging.getLogger(__name__)
class BaseMicroversionTest(object):
@@ -166,7 +164,6 @@
if op is None:
msg = ("Operation %s is invalid. Valid options include: lt, eq, gt, "
"le, ne, ge." % operation)
- LOG.debug(msg)
raise exceptions.InvalidParam(invalid_param=msg)
# Remove "volume" from "volume <microversion>", for example, so that the
diff --git a/tempest/lib/common/http.py b/tempest/lib/common/http.py
index b4b1fc9..738c37f 100644
--- a/tempest/lib/common/http.py
+++ b/tempest/lib/common/http.py
@@ -17,6 +17,47 @@
import urllib3
+class ClosingProxyHttp(urllib3.ProxyManager):
+ def __init__(self, proxy_url, disable_ssl_certificate_validation=False,
+ ca_certs=None, timeout=None):
+ kwargs = {}
+
+ if disable_ssl_certificate_validation:
+ urllib3.disable_warnings()
+ kwargs['cert_reqs'] = 'CERT_NONE'
+ elif ca_certs:
+ kwargs['cert_reqs'] = 'CERT_REQUIRED'
+ kwargs['ca_certs'] = ca_certs
+
+ if timeout:
+ kwargs['timeout'] = timeout
+
+ super(ClosingProxyHttp, self).__init__(proxy_url, **kwargs)
+
+ def request(self, url, method, *args, **kwargs):
+
+ class Response(dict):
+ def __init__(self, info):
+ for key, value in info.getheaders().items():
+ self[key.lower()] = value
+ self.status = info.status
+ self['status'] = str(self.status)
+ self.reason = info.reason
+ self.version = info.version
+ self['content-location'] = url
+
+ original_headers = kwargs.get('headers', {})
+ new_headers = dict(original_headers, connection='close')
+ new_kwargs = dict(kwargs, headers=new_headers)
+
+ # Follow up to 5 redirections. Don't raise an exception if
+ # it's exceeded but return the HTTP 3XX response instead.
+ retry = urllib3.util.Retry(raise_on_redirect=False, redirect=5)
+ r = super(ClosingProxyHttp, self).request(method, url, retries=retry,
+ *args, **new_kwargs)
+ return Response(r), r.data
+
+
class ClosingHttp(urllib3.poolmanager.PoolManager):
def __init__(self, disable_ssl_certificate_validation=False,
ca_certs=None, timeout=None):
diff --git a/tempest/lib/common/rest_client.py b/tempest/lib/common/rest_client.py
index f58d737..22276d4 100644
--- a/tempest/lib/common/rest_client.py
+++ b/tempest/lib/common/rest_client.py
@@ -69,6 +69,7 @@
of the request and response payload
:param str http_timeout: Timeout in seconds to wait for the http request to
return
+ :param str proxy_url: http proxy url to use.
"""
# The version of the API this client implements
@@ -80,7 +81,8 @@
endpoint_type='publicURL',
build_interval=1, build_timeout=60,
disable_ssl_certificate_validation=False, ca_certs=None,
- trace_requests='', name=None, http_timeout=None):
+ trace_requests='', name=None, http_timeout=None,
+ proxy_url=None):
self.auth_provider = auth_provider
self.service = service
self.region = region
@@ -100,9 +102,16 @@
'retry-after', 'server',
'vary', 'www-authenticate'))
dscv = disable_ssl_certificate_validation
- self.http_obj = http.ClosingHttp(
- disable_ssl_certificate_validation=dscv, ca_certs=ca_certs,
- timeout=http_timeout)
+
+ if proxy_url:
+ self.http_obj = http.ClosingProxyHttp(
+ proxy_url,
+ disable_ssl_certificate_validation=dscv, ca_certs=ca_certs,
+ timeout=http_timeout)
+ else:
+ self.http_obj = http.ClosingHttp(
+ disable_ssl_certificate_validation=dscv, ca_certs=ca_certs,
+ timeout=http_timeout)
def get_headers(self, accept_type=None, send_type=None):
"""Return the default headers which will be used with outgoing requests
diff --git a/tempest/lib/common/utils/data_utils.py b/tempest/lib/common/utils/data_utils.py
index a0941ef..c5df590 100644
--- a/tempest/lib/common/utils/data_utils.py
+++ b/tempest/lib/common/utils/data_utils.py
@@ -18,9 +18,6 @@
import string
import uuid
-from debtcollector import removals
-import netaddr
-from oslo_utils import netutils
from oslo_utils import uuidutils
import six.moves
@@ -177,36 +174,6 @@
for i in range(size)])
-@removals.remove(
- message="use get_ipv6_addr_by_EUI64 from oslo_utils.netutils",
- version="Newton",
- removal_version="Ocata")
-def get_ipv6_addr_by_EUI64(cidr, mac):
- """Generate a IPv6 addr by EUI-64 with CIDR and MAC
-
- :param str cidr: a IPv6 CIDR
- :param str mac: a MAC address
- :return: an IPv6 Address
- :rtype: netaddr.IPAddress
- """
- # Check if the prefix is IPv4 address
- is_ipv4 = netutils.is_valid_ipv4(cidr)
- if is_ipv4:
- msg = "Unable to generate IP address by EUI64 for IPv4 prefix"
- raise TypeError(msg)
- try:
- eui64 = int(netaddr.EUI(mac).eui64())
- prefix = netaddr.IPNetwork(cidr)
- return netaddr.IPAddress(prefix.first + eui64 ^ (1 << 57))
- except (ValueError, netaddr.AddrFormatError):
- raise TypeError('Bad prefix or mac format for generating IPv6 '
- 'address by EUI-64: %(prefix)s, %(mac)s:'
- % {'prefix': cidr, 'mac': mac})
- except TypeError:
- raise TypeError('Bad prefix type for generate IPv6 address by '
- 'EUI-64: %s' % cidr)
-
-
# Courtesy of http://stackoverflow.com/a/312464
def chunkify(sequence, chunksize):
"""Yield successive chunks from `sequence`."""
diff --git a/tempest/lib/common/utils/test_utils.py b/tempest/lib/common/utils/test_utils.py
index bd0db7c..c2e93ee 100644
--- a/tempest/lib/common/utils/test_utils.py
+++ b/tempest/lib/common/utils/test_utils.py
@@ -86,22 +86,29 @@
pass
-def call_until_true(func, duration, sleep_for):
+def call_until_true(func, duration, sleep_for, *args, **kwargs):
"""Call the given function until it returns True (and return True)
or until the specified duration (in seconds) elapses (and return False).
- :param func: A zero argument callable that returns True on success.
+ :param func: A callable that returns True on success.
:param duration: The number of seconds for which to attempt a
successful call of the function.
:param sleep_for: The number of seconds to sleep after an unsuccessful
invocation of the function.
+ :param args: args that are passed to func.
+ :param kwargs: kwargs that are passed to func.
"""
now = time.time()
+ begin_time = now
timeout = now + duration
while now < timeout:
- if func():
+ if func(*args, **kwargs):
+ LOG.debug("Call %s returns true in %f seconds",
+ getattr(func, '__name__'), time.time() - begin_time)
return True
time.sleep(sleep_for)
now = time.time()
+ LOG.debug("Call %s returns false in %f seconds",
+ getattr(func, '__name__'), duration)
return False
diff --git a/tempest/common/validation_resources.py b/tempest/lib/common/validation_resources.py
similarity index 75%
rename from tempest/common/validation_resources.py
rename to tempest/lib/common/validation_resources.py
index 3a04b9a..c35a01a 100644
--- a/tempest/common/validation_resources.py
+++ b/tempest/lib/common/validation_resources.py
@@ -12,6 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+import fixtures
from oslo_log import log as logging
from oslo_utils import excutils
@@ -180,6 +181,9 @@
floating_ip = resources['floating_ip']['ip']
"""
# Create and Return the validation resources required to validate a VM
+ msg = ('Requested validation resources keypair %s, floating IP %s, '
+ 'security group %s')
+ LOG.debug(msg, keypair, floating_ip, security_group)
validation_data = {}
try:
if keypair:
@@ -340,3 +344,114 @@
has_exception = exc
if has_exception:
raise has_exception
+
+
+class ValidationResourcesFixture(fixtures.Fixture):
+ """Fixture to provision and cleanup validation resources"""
+
+ DICT_KEYS = ['keypair', 'security_group', 'floating_ip']
+
+ def __init__(self, clients, keypair=False, floating_ip=False,
+ security_group=False, security_group_rules=False,
+ ethertype='IPv4', use_neutron=True, floating_network_id=None,
+ floating_network_name=None):
+ """Create a ValidationResourcesFixture
+
+ Create a ValidationResourcesFixture fixtures, which provisions the
+ resources required to be able to ping / ssh a virtual machine upon
+ setUp and clears them out upon cleanup. Resources are keypair,
+ security group, security group rules and a floating IP - depending
+ on the params.
+
+ The fixture exposes a dictionary that includes provisioned resources.
+
+ :param clients: `tempest.lib.services.clients.ServiceClients` or of a
+ subclass of it. Resources are provisioned using clients from
+ `clients`.
+ :param keypair: Whether to provision a keypair. Defaults to False.
+ :param floating_ip: Whether to provision a floating IP.
+ Defaults to False.
+ :param security_group: Whether to provision a security group.
+ Defaults to False.
+ :param security_group_rules: Whether to provision security group rules.
+ Defaults to False.
+ :param ethertype: 'IPv4' or 'IPv6'. Honoured only if neutron is used.
+ :param use_neutron: When True resources are provisioned via neutron,
+ when False resources are provisioned via nova.
+ :param floating_network_id: The id of the network used to provision a
+ floating IP. Only used if a floating IP is requested in case
+ neutron is used.
+ :param floating_network_name: The name of the floating IP pool used to
+ provision the floating IP. Only used if a floating IP is requested
+ and with nova-net.
+ :returns: A dictionary with the same keys as the input
+ `validation_resources` and the resources for values in the format
+ they are returned by the API.
+
+ Examples::
+
+ from tempest.common import validation_resources as vr
+ from tempest.lib import auth
+ from tempest.lib.services import clients
+ import testtools
+
+
+ class TestWithVR(testtools.TestCase):
+
+ def setUp(self):
+ creds = auth.get_credentials(
+ 'http://mycloud/identity/v3',
+ username='me', project_name='me',
+ password='secret', domain_name='Default')
+
+ osclients = clients.ServiceClients(
+ creds, 'http://mycloud/identity/v3')
+ # Request keypair and floating IP
+ resources = dict(keypair=True, security_group=False,
+ security_group_rules=False,
+ floating_ip=True)
+ network_id = '4240E68E-23DA-4C82-AC34-9FEFAA24521C'
+ self.vr = self.useFixture(vr.ValidationResourcesFixture(
+ osclients, use_neutron=True,
+ floating_network_id=network_id,
+ **resources)
+
+ def test_use_ip(self):
+ # The floating IP to be attached to the VM
+ floating_ip = self.vr['floating_ip']['ip']
+ """
+ self._clients = clients
+ self._keypair = keypair
+ self._floating_ip = floating_ip
+ self._security_group = security_group
+ self._security_group_rules = security_group_rules
+ self._ethertype = ethertype
+ self._use_neutron = use_neutron
+ self._floating_network_id = floating_network_id
+ self._floating_network_name = floating_network_name
+ self._validation_resources = None
+
+ def _setUp(self):
+ msg = ('Requested setup of ValidationResources keypair %s, floating '
+ 'IP %s, security group %s')
+ LOG.debug(msg, self._keypair, self._floating_ip, self._security_group)
+ self._validation_resources = create_validation_resources(
+ self._clients, keypair=self._keypair,
+ floating_ip=self._floating_ip,
+ security_group=self._security_group,
+ security_group_rules=self._security_group_rules,
+ ethertype=self._ethertype, use_neutron=self._use_neutron,
+ floating_network_id=self._floating_network_id,
+ floating_network_name=self._floating_network_name)
+ # If provisioning raises an exception we won't have anything to
+ # cleanup here, so we don't need a try-finally around provisioning
+ vr = self._validation_resources
+ self.addCleanup(clear_validation_resources, self._clients,
+ keypair=vr.get('keypair', None),
+ floating_ip=vr.get('floating_ip', None),
+ security_group=vr.get('security_group', None),
+ use_neutron=self._use_neutron)
+
+ @property
+ def resources(self):
+ return self._validation_resources
diff --git a/tempest/lib/decorators.py b/tempest/lib/decorators.py
index f82f707..e99dd24 100644
--- a/tempest/lib/decorators.py
+++ b/tempest/lib/decorators.py
@@ -15,7 +15,6 @@
import functools
import uuid
-import debtcollector.removals
from oslo_log import log as logging
import six
import testtools
@@ -31,7 +30,7 @@
"""
def decorator(f):
@functools.wraps(f)
- def wrapper(self, *func_args, **func_kwargs):
+ def wrapper(*func_args, **func_kwargs):
skip = False
if "condition" in kwargs:
if kwargs["condition"] is True:
@@ -43,7 +42,7 @@
raise ValueError('bug must be a valid bug number')
msg = "Skipped until Bug: %s is resolved." % kwargs["bug"]
raise testtools.TestCase.skipException(msg)
- return f(self, *func_args, **func_kwargs)
+ return f(*func_args, **func_kwargs)
return wrapper
return decorator
@@ -56,9 +55,9 @@
"""
def decorator(f):
@functools.wraps(f)
- def wrapper(self, *func_args, **func_kwargs):
+ def wrapper(*func_args, **func_kwargs):
try:
- return f(self, *func_args, **func_kwargs)
+ return f(*func_args, **func_kwargs)
except Exception as exc:
exc_status_code = getattr(exc, 'status_code', None)
if status_code is None or status_code == exc_status_code:
@@ -87,25 +86,6 @@
return decorator
-@debtcollector.removals.remove(removal_version='Queen')
-class skip_unless_attr(object):
- """Decorator to skip tests if a specified attr does not exists or False"""
- def __init__(self, attr, msg=None):
- self.attr = attr
- self.message = msg or ("Test case attribute %s not found "
- "or False") % attr
-
- def __call__(self, func):
- @functools.wraps(func)
- def _skipper(*args, **kw):
- """Wrapped skipper function."""
- testobj = args[0]
- if not getattr(testobj, self.attr, False):
- raise testtools.TestCase.skipException(self.message)
- func(*args, **kw)
- return _skipper
-
-
def attr(**kwargs):
"""A decorator which applies the testtools attr decorator
diff --git a/tempest/lib/exceptions.py b/tempest/lib/exceptions.py
index 9b2e87e..13af890 100644
--- a/tempest/lib/exceptions.py
+++ b/tempest/lib/exceptions.py
@@ -96,7 +96,7 @@
class Conflict(ClientRestClientException):
status_code = 409
- message = "An object with that identifier already exists"
+ message = "Conflict with state of target resource"
class Gone(ClientRestClientException):
diff --git a/tempest/lib/services/clients.py b/tempest/lib/services/clients.py
index 4fa7a7a..8918a8c 100644
--- a/tempest/lib/services/clients.py
+++ b/tempest/lib/services/clients.py
@@ -31,6 +31,7 @@
from tempest.lib.services import identity
from tempest.lib.services import image
from tempest.lib.services import network
+from tempest.lib.services import object_storage
from tempest.lib.services import volume
warnings.simplefilter("once")
@@ -50,20 +51,13 @@
'image.v1': image.v1,
'image.v2': image.v2,
'network': network,
+ 'object-storage': object_storage,
'volume.v1': volume.v1,
'volume.v2': volume.v2,
'volume.v3': volume.v3
}
-def _tempest_internal_modules():
- # Set of unstable service clients available in Tempest
- # NOTE(andreaf) This list will exists only as long the remain clients
- # are migrated to tempest.lib, and it will then be deleted without
- # deprecation or advance notice
- return set(['object-storage'])
-
-
def available_modules():
"""Set of service client modules available in Tempest and plugins
@@ -101,17 +95,6 @@
plug_service_versions))
name_conflicts.append(exceptions.PluginRegistrationException(
name=plugin_name, detailed_error=detailed_error))
- # NOTE(andreaf) Once all tempest clients are stable, the following
- # if will have to be removed.
- if not plug_service_versions.isdisjoint(
- _tempest_internal_modules()):
- detailed_error = (
- 'Plugin %s is trying to register a service %s already '
- 'claimed by a Tempest one' % (plugin_name,
- _tempest_internal_modules() &
- plug_service_versions))
- name_conflicts.append(exceptions.PluginRegistrationException(
- name=plugin_name, detailed_error=detailed_error))
extra_service_versions |= plug_service_versions
if name_conflicts:
LOG.error(
@@ -276,7 +259,7 @@
@removals.removed_kwarg('client_parameters')
def __init__(self, credentials, identity_uri, region=None, scope='project',
disable_ssl_certificate_validation=True, ca_certs=None,
- trace_requests='', client_parameters=None):
+ trace_requests='', client_parameters=None, proxy_url=None):
"""Service Clients provider
Instantiate a `ServiceClients` object, from a set of credentials and an
@@ -336,6 +319,8 @@
name, as declared in `service_clients.available_modules()` except
for the version. Values are dictionaries of parameters that are
going to be passed to all clients in the service client module.
+ :param proxy_url: Applies to auth and to all service clients, set a
+ proxy url for the clients to use.
"""
self._registered_services = set([])
self.credentials = credentials
@@ -360,16 +345,20 @@
self.dscv = disable_ssl_certificate_validation
self.ca_certs = ca_certs
self.trace_requests = trace_requests
+ self.proxy_url = proxy_url
# Creates an auth provider for the credentials
self.auth_provider = auth_provider_class(
self.credentials, self.identity_uri, scope=scope,
disable_ssl_certificate_validation=self.dscv,
- ca_certs=self.ca_certs, trace_requests=self.trace_requests)
+ ca_certs=self.ca_certs, trace_requests=self.trace_requests,
+ proxy_url=proxy_url)
+
# Setup some defaults for client parameters of registered services
client_parameters = client_parameters or {}
self.parameters = {}
+
# Parameters are provided for unversioned services
- all_modules = available_modules() | _tempest_internal_modules()
+ all_modules = available_modules()
unversioned_services = set(
[x.split('.')[0] for x in all_modules])
for service in unversioned_services:
@@ -420,8 +409,8 @@
clients in tempest.
:param client_names: List or set of names of service client classes.
:param kwargs: Extra optional parameters to be passed to all clients.
- ServiceClient provides defaults for region, dscv, ca_certs and
- trace_requests.
+ ServiceClient provides defaults for region, dscv, ca_certs, http
+ proxies and trace_requests.
:raise ServiceClientRegistrationException: if the provided name is
already in use or if service_version is already registered.
:raise ImportError: if module_path cannot be imported.
@@ -442,7 +431,8 @@
params = dict(region=self.region,
disable_ssl_certificate_validation=self.dscv,
ca_certs=self.ca_certs,
- trace_requests=self.trace_requests)
+ trace_requests=self.trace_requests,
+ proxy_url=self.proxy_url)
params.update(kwargs)
# Instantiate the client factory
_factory = ClientsFactory(module_path=module_path,
@@ -456,9 +446,7 @@
@property
def registered_services(self):
- # NOTE(andreaf) Once all tempest modules are stable this needs to
- # be updated to remove _tempest_internal_modules
- return self._registered_services | _tempest_internal_modules()
+ return self._registered_services
def _setup_parameters(self, parameters):
"""Setup default values for client parameters
diff --git a/tempest/lib/services/identity/v3/identity_client.py b/tempest/lib/services/identity/v3/identity_client.py
index 2512a3e..ad770bf 100644
--- a/tempest/lib/services/identity/v3/identity_client.py
+++ b/tempest/lib/services/identity/v3/identity_client.py
@@ -57,3 +57,10 @@
self.expected_success(200, resp.status)
body = json.loads(body)
return rest_client.ResponseBody(resp, body)
+
+ def list_auth_domains(self):
+ """Get available domain scopes."""
+ resp, body = self.get("auth/domains")
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/object_storage/__init__.py b/tempest/lib/services/object_storage/__init__.py
index e69de29..4303d09 100644
--- a/tempest/lib/services/object_storage/__init__.py
+++ b/tempest/lib/services/object_storage/__init__.py
@@ -0,0 +1,25 @@
+# Copyright (c) 2016 Hewlett-Packard Enterprise Development Company, L.P.
+#
+# 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.object_storage.account_client import AccountClient
+from tempest.lib.services.object_storage.bulk_middleware_client import \
+ BulkMiddlewareClient
+from tempest.lib.services.object_storage.capabilities_client import \
+ CapabilitiesClient
+from tempest.lib.services.object_storage.container_client import \
+ ContainerClient
+from tempest.lib.services.object_storage.object_client import ObjectClient
+
+__all__ = ['AccountClient', 'BulkMiddlewareClient', 'CapabilitiesClient',
+ 'ContainerClient', 'ObjectClient']
diff --git a/tempest/services/object_storage/account_client.py b/tempest/lib/services/object_storage/account_client.py
similarity index 75%
rename from tempest/services/object_storage/account_client.py
rename to tempest/lib/services/object_storage/account_client.py
index 5a1737e..67f01a6 100644
--- a/tempest/services/object_storage/account_client.py
+++ b/tempest/lib/services/object_storage/account_client.py
@@ -50,41 +50,20 @@
return resp, body
def list_account_metadata(self):
- """HEAD on the storage URL
-
- Returns all account metadata headers
- """
+ """List all account metadata."""
resp, body = self.head('')
self.expected_success(204, resp.status)
return resp, body
def list_account_containers(self, params=None):
- """GET on the (base) storage URL
+ """List all containers for the account.
Given valid X-Auth-Token, returns a list of all containers for the
account.
- Optional Arguments:
- limit=[integer value N]
- Limits the number of results to at most N values
- DEFAULT: 10,000
-
- marker=[string value X]
- Given string value X, return object names greater in value
- than the specified marker.
- DEFAULT: No Marker
-
- prefix=[string value Y]
- Given string value Y, return object names starting with that prefix
-
- reverse=[boolean value Z]
- Reverse the result order based on the boolean value Z
- DEFAULT: False
-
- format=[string value, either 'json' or 'xml']
- Specify either json or xml to return the respective serialized
- response.
- DEFAULT: Python-List returned in response body
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/object-store/#show-account-details-and-list-containers
"""
url = '?%s' % urllib.urlencode(params) if params else ''
diff --git a/tempest/lib/services/object_storage/container_client.py b/tempest/lib/services/object_storage/container_client.py
new file mode 100644
index 0000000..2da8e24
--- /dev/null
+++ b/tempest/lib/services/object_storage/container_client.py
@@ -0,0 +1,124 @@
+# Copyright 2012 OpenStack Foundation
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from xml.etree import ElementTree as etree
+
+import debtcollector.moves
+from oslo_serialization import jsonutils as json
+from six.moves.urllib import parse as urllib
+
+from tempest.lib.common import rest_client
+
+
+class ContainerClient(rest_client.RestClient):
+
+ def update_container(self, container_name, **headers):
+ """Creates or Updates a container
+
+ with optional metadata passed in as a dictionary.
+ Full list of allowed headers or value, please refer to the
+ official API reference:
+ https://developer.openstack.org/api-ref/object-store/#create-container
+ """
+ url = str(container_name)
+
+ resp, body = self.put(url, body=None, headers=headers)
+ self.expected_success([201, 202], resp.status)
+ return resp, body
+
+ # NOTE: This alias is for the usability because PUT can be used for both
+ # updating/creating a resource and this PUT is mainly used for creating
+ # on Swift container API.
+ create_container = update_container
+
+ def delete_container(self, container_name):
+ """Deletes the container (if it's empty)."""
+ url = str(container_name)
+ resp, body = self.delete(url)
+ self.expected_success(204, resp.status)
+ return resp, body
+
+ def create_update_or_delete_container_metadata(
+ self, container_name,
+ create_update_metadata=None,
+ delete_metadata=None,
+ create_update_metadata_prefix='X-Container-Meta-',
+ delete_metadata_prefix='X-Remove-Container-Meta-'):
+ """Creates, Updates or deletes an containter metadata entry.
+
+ Container Metadata can be created, updated or deleted based on
+ metadata header or value. For detailed info, please refer to the
+ official API reference:
+ https://developer.openstack.org/api-ref/object-store/#create-update-or-delete-container-metadata
+ """
+ url = str(container_name)
+ headers = {}
+ if create_update_metadata:
+ for key in create_update_metadata:
+ metadata_header_name = create_update_metadata_prefix + key
+ headers[metadata_header_name] = create_update_metadata[key]
+ if delete_metadata:
+ for key in delete_metadata:
+ headers[delete_metadata_prefix + key] = delete_metadata[key]
+
+ resp, body = self.post(url, headers=headers, body=None)
+ self.expected_success(204, resp.status)
+ return resp, body
+
+ update_container_metadata = debtcollector.moves.moved_function(
+ create_update_or_delete_container_metadata,
+ 'update_container_metadata', __name__,
+ version='Queens', removal_version='Rocky')
+
+ def list_container_metadata(self, container_name):
+ """List all container metadata."""
+ url = str(container_name)
+ resp, body = self.head(url)
+ self.expected_success(204, resp.status)
+ return resp, body
+
+ def list_container_objects(self, container_name, params=None):
+ """List the objects in a container, given the container name
+
+ Returns the container object listing as a plain text list, or as
+ xml or json if that option is specified via the 'format' argument.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/object-storage/?expanded=show-container-details-and-list-objects-detail
+ """
+
+ url = str(container_name)
+ if params:
+ url += '?'
+ url += '&%s' % urllib.urlencode(params)
+
+ resp, body = self.get(url, headers={})
+ if params and params.get('format') == 'json':
+ body = json.loads(body)
+ elif params and params.get('format') == 'xml':
+ body = etree.fromstring(body)
+ # Else the content-type is plain/text
+ else:
+ body = [
+ obj_name for obj_name in body.decode().split('\n') if obj_name
+ ]
+
+ self.expected_success([200, 204], resp.status)
+ return resp, body
+
+ list_container_contents = debtcollector.moves.moved_function(
+ list_container_objects, 'list_container_contents', __name__,
+ version='Queens', removal_version='Rocky')
diff --git a/tempest/services/object_storage/object_client.py b/tempest/lib/services/object_storage/object_client.py
similarity index 62%
rename from tempest/services/object_storage/object_client.py
rename to tempest/lib/services/object_storage/object_client.py
index 6d656ec..383aff6 100644
--- a/tempest/services/object_storage/object_client.py
+++ b/tempest/lib/services/object_storage/object_client.py
@@ -23,7 +23,8 @@
class ObjectClient(rest_client.RestClient):
def create_object(self, container, object_name, data,
- params=None, metadata=None, headers=None):
+ params=None, metadata=None, headers=None,
+ chunked=False):
"""Create storage object."""
if headers is None:
@@ -37,7 +38,7 @@
if params:
url += '?%s' % urlparse.urlencode(params)
- resp, body = self.put(url, data, headers)
+ resp, body = self.put(url, data, headers, chunked=chunked)
self.expected_success(201, resp.status)
return resp, body
@@ -50,28 +51,27 @@
self.expected_success([200, 204], resp.status)
return resp, body
- def update_object_metadata(self, container, object_name, metadata,
- metadata_prefix='X-Object-Meta-'):
+ def create_or_update_object_metadata(self, container, object_name,
+ headers=None):
"""Add, remove, or change X-Object-Meta metadata for storage object."""
- headers = {}
- for key in metadata:
- headers["%s%s" % (str(metadata_prefix), str(key))] = metadata[key]
-
url = "%s/%s" % (str(container), str(object_name))
resp, body = self.post(url, None, headers=headers)
self.expected_success(202, resp.status)
return resp, body
- def list_object_metadata(self, container, object_name):
+ def list_object_metadata(self, container, object_name,
+ params=None, headers=None):
"""List all storage object X-Object-Meta- metadata."""
url = "%s/%s" % (str(container), str(object_name))
- resp, body = self.head(url)
+ if params:
+ url += '?%s' % urlparse.urlencode(params)
+ resp, body = self.head(url, headers=headers)
self.expected_success(200, resp.status)
return resp, body
- def get_object(self, container, object_name, metadata=None):
+ def get_object(self, container, object_name, metadata=None, params=None):
"""Retrieve object's data."""
headers = {}
@@ -80,45 +80,12 @@
headers[str(key)] = metadata[key]
url = "{0}/{1}".format(container, object_name)
+ if params:
+ url += '?%s' % urlparse.urlencode(params)
resp, body = self.get(url, headers=headers)
self.expected_success([200, 206], resp.status)
return resp, body
- def copy_object_in_same_container(self, container, src_object_name,
- dest_object_name, metadata=None):
- """Copy storage object's data to the new object using PUT."""
-
- url = "{0}/{1}".format(container, dest_object_name)
- headers = {}
- headers['X-Copy-From'] = "%s/%s" % (str(container),
- str(src_object_name))
- headers['content-length'] = '0'
- if metadata:
- for key in metadata:
- headers[str(key)] = metadata[key]
-
- resp, body = self.put(url, None, headers=headers)
- self.expected_success(201, resp.status)
- return resp, body
-
- def copy_object_across_containers(self, src_container, src_object_name,
- dst_container, dst_object_name,
- metadata=None):
- """Copy storage object's data to the new object using PUT."""
-
- url = "{0}/{1}".format(dst_container, dst_object_name)
- headers = {}
- headers['X-Copy-From'] = "%s/%s" % (str(src_container),
- str(src_object_name))
- headers['content-length'] = '0'
- if metadata:
- for key in metadata:
- headers[str(key)] = metadata[key]
-
- resp, body = self.put(url, None, headers=headers)
- self.expected_success(201, resp.status)
- return resp, body
-
def copy_object_2d_way(self, container, src_object_name, dest_object_name,
metadata=None):
"""Copy storage object's data to the new object using COPY."""
@@ -135,38 +102,6 @@
self.expected_success(201, resp.status)
return resp, body
- def create_object_segments(self, container, object_name, segment, data):
- """Creates object segments."""
- url = "{0}/{1}/{2}".format(container, object_name, segment)
- resp, body = self.put(url, data)
- self.expected_success(201, resp.status)
- return resp, body
-
- def put_object_with_chunk(self, container, name, contents):
- """Put an object with Transfer-Encoding header
-
- :param container: name of the container
- :type container: string
- :param name: name of the object
- :type name: string
- :param contents: object data
- :type contents: iterable
- """
- headers = {'Transfer-Encoding': 'chunked'}
- if self.token:
- headers['X-Auth-Token'] = self.token
-
- url = "%s/%s" % (container, name)
- resp, body = self.put(
- url, headers=headers,
- body=contents,
- chunked=True
- )
-
- self._error_checker(resp, body)
- self.expected_success(201, resp.status)
- return resp.status, resp.reason, resp
-
def create_object_continue(self, container, object_name,
data, metadata=None):
"""Put an object using Expect:100-continue"""
@@ -183,8 +118,7 @@
path = str(parsed.path) + "/"
path += "%s/%s" % (str(container), str(object_name))
- conn = create_connection(parsed)
-
+ conn = _create_connection(parsed)
# Send the PUT request and the headers including the "Expect" header
conn.putrequest('PUT', path)
@@ -218,7 +152,7 @@
return resp.status, resp.reason
-def create_connection(parsed_url):
+def _create_connection(parsed_url):
"""Helper function to create connection with httplib
:param parsed_url: parsed url of the remote location
diff --git a/tempest/lib/services/volume/v1/encryption_types_client.py b/tempest/lib/services/volume/v1/encryption_types_client.py
old mode 100755
new mode 100644
index 067b4e8..0fac6bd
--- a/tempest/lib/services/volume/v1/encryption_types_client.py
+++ b/tempest/lib/services/volume/v1/encryption_types_client.py
@@ -49,9 +49,9 @@
def create_encryption_type(self, volume_type_id, **kwargs):
"""Create encryption type.
- TODO: Current api-site doesn't contain this API description.
- After fixing the api-site, we need to fix here also for putting
- the link to api-site.
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/block-storage/v2/#create-an-encryption-type-for-v2
"""
url = "/types/%s/encryption" % volume_type_id
post_body = json.dumps({'encryption': kwargs})
diff --git a/tempest/lib/services/volume/v1/hosts_client.py b/tempest/lib/services/volume/v1/hosts_client.py
index 56ba12c..9b19b84 100644
--- a/tempest/lib/services/volume/v1/hosts_client.py
+++ b/tempest/lib/services/volume/v1/hosts_client.py
@@ -23,8 +23,12 @@
"""Client class to send CRUD Volume Host API V1 requests"""
def list_hosts(self, **params):
- """Lists all hosts."""
+ """Lists all hosts.
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/block-storage/v2/#list-all-hosts
+ """
url = 'os-hosts'
if params:
url += '?%s' % urllib.urlencode(params)
diff --git a/tempest/lib/services/volume/v1/qos_client.py b/tempest/lib/services/volume/v1/qos_client.py
index e247b7b..593bddd 100644
--- a/tempest/lib/services/volume/v1/qos_client.py
+++ b/tempest/lib/services/volume/v1/qos_client.py
@@ -92,7 +92,9 @@
:param keys: keys to delete from the QoS specification.
- TODO(jordanP): Add a link once LP #1524877 is fixed.
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/block-storage/v2/#unset-keys-in-qos-specification
"""
put_body = json.dumps({'keys': keys})
resp, body = self.put('qos-specs/%s/delete_keys' % qos_id, put_body)
diff --git a/tempest/lib/services/volume/v1/quotas_client.py b/tempest/lib/services/volume/v1/quotas_client.py
index 678fd82..84f34f2 100644
--- a/tempest/lib/services/volume/v1/quotas_client.py
+++ b/tempest/lib/services/volume/v1/quotas_client.py
@@ -47,7 +47,7 @@
For a full list of available parameters, please refer to the official
API reference:
- http://developer.openstack.org/api-ref-blockstorage-v1.html#updateQuota
+ https://developer.openstack.org/api-ref/block-storage/v2/#update-quotas
"""
put_body = jsonutils.dumps({'quota_set': kwargs})
resp, body = self.put('os-quota-sets/%s' % tenant_id, put_body)
diff --git a/tempest/lib/services/volume/v1/snapshots_client.py b/tempest/lib/services/volume/v1/snapshots_client.py
index 3433e68..51f7b9b 100644
--- a/tempest/lib/services/volume/v1/snapshots_client.py
+++ b/tempest/lib/services/volume/v1/snapshots_client.py
@@ -27,7 +27,8 @@
For a full list of available parameters, please refer to the official
API reference:
- http://developer.openstack.org/api-ref/block-storage/v1/#list-snapshots-with-details-v1
+ https://developer.openstack.org/api-ref/block-storage/v2/#list-snapshots
+ https://developer.openstack.org/api-ref/block-storage/v2/#list-snapshots-with-details
"""
url = 'snapshots'
if detail:
@@ -45,7 +46,7 @@
For a full list of available parameters, please refer to the official
API reference:
- http://developer.openstack.org/api-ref/block-storage/v1/#show-snapshot-details-v1
+ https://developer.openstack.org/api-ref/block-storage/v2/#show-snapshot-details
"""
url = "snapshots/%s" % snapshot_id
resp, body = self.get(url)
@@ -58,7 +59,7 @@
For a full list of available parameters, please refer to the official
API reference:
- http://developer.openstack.org/api-ref/block-storage/v1/#create-snapshot-v1
+ https://developer.openstack.org/api-ref/block-storage/v2/#create-snapshot
"""
post_body = json.dumps({'snapshot': kwargs})
resp, body = self.post('snapshots', post_body)
@@ -71,7 +72,7 @@
For a full list of available parameters, please refer to the official
API reference:
- http://developer.openstack.org/api-ref/block-storage/v1/#delete-snapshot-v1
+ https://developer.openstack.org/api-ref/block-storage/v2/#delete-snapshot
"""
resp, body = self.delete("snapshots/%s" % snapshot_id)
self.expected_success(202, resp.status)
@@ -123,7 +124,7 @@
For a full list of available parameters, please refer to the official
API reference:
- http://developer.openstack.org/api-ref/block-storage/v1/#update-snapshot-v1
+ https://developer.openstack.org/api-ref/block-storage/v2/#update-snapshot
"""
put_body = json.dumps({'snapshot': kwargs})
resp, body = self.put('snapshots/%s' % snapshot_id, put_body)
@@ -136,7 +137,7 @@
For a full list of available parameters, please refer to the official
API reference:
- http://developer.openstack.org/api-ref/block-storage/v1/#show-snapshot-metadata-v1
+ https://developer.openstack.org/api-ref/block-storage/v2/#show-snapshot-metadata
"""
url = "snapshots/%s/metadata" % snapshot_id
resp, body = self.get(url)
@@ -149,7 +150,7 @@
For a full list of available parameters, please refer to the official
API reference:
- http://developer.openstack.org/api-ref/block-storage/v1/#update-snapshot-metadata-v1
+ https://developer.openstack.org/api-ref/block-storage/v2/#update-snapshot-metadata
"""
put_body = json.dumps(kwargs)
url = "snapshots/%s/metadata" % snapshot_id
diff --git a/tempest/lib/services/volume/v1/types_client.py b/tempest/lib/services/volume/v1/types_client.py
index 4ae9935..58a80b7 100644
--- a/tempest/lib/services/volume/v1/types_client.py
+++ b/tempest/lib/services/volume/v1/types_client.py
@@ -40,7 +40,7 @@
For a full list of available parameters, please refer to the official
API reference:
- http://developer.openstack.org/api-ref/block-storage/v1/#list-volume-types-v1
+ https://developer.openstack.org/api-ref/block-storage/v2/#list-all-volume-types-for-v2
"""
url = 'types'
if params:
@@ -56,7 +56,7 @@
For a full list of available parameters, please refer to the official
API reference:
- http://developer.openstack.org/api-ref/block-storage/v1/#show-volume-type-v1
+ https://developer.openstack.org/api-ref/block-storage/v2/#show-volume-type-details-for-v2
"""
url = "types/%s" % volume_type_id
resp, body = self.get(url)
@@ -69,7 +69,7 @@
For a full list of available parameters, please refer to the official
API reference:
- http://developer.openstack.org/api-ref/block-storage/v1/#create-volume-type-v1
+ https://developer.openstack.org/api-ref/block-storage/v2/#create-volume-type-for-v2
"""
post_body = json.dumps({'volume_type': kwargs})
resp, body = self.post('types', post_body)
@@ -82,7 +82,7 @@
For a full list of available parameters, please refer to the official
API reference:
- http://developer.openstack.org/api-ref/block-storage/v1/#delete-volume-type-v1
+ https://developer.openstack.org/api-ref/block-storage/v2/#delete-volume-type
"""
resp, body = self.delete("types/%s" % volume_type_id)
self.expected_success(202, resp.status)
@@ -137,7 +137,7 @@
For a full list of available parameters, please refer to the official
API reference:
- http://developer.openstack.org/api-ref/block-storage/v1/#update-volume-type-v1
+ https://developer.openstack.org/api-ref/block-storage/v2/#update-volume-type
"""
put_body = json.dumps({'volume_type': kwargs})
resp, body = self.put('types/%s' % volume_type_id, put_body)
@@ -155,7 +155,7 @@
updated value.
For a full list of available parameters, please refer to the official
API reference:
- http://developer.openstack.org/api-ref/block-storage/v1/#update-extra-specs-for-a-volume-type-v1
+ https://developer.openstack.org/api-ref/block-storage/v2/#update-extra-specs-for-a-volume-type
"""
url = "types/%s/extra_specs/%s" % (volume_type_id, extra_spec_name)
put_body = json.dumps(extra_specs)
diff --git a/tempest/lib/services/volume/v1/volumes_client.py b/tempest/lib/services/volume/v1/volumes_client.py
index 7a25697..0e6ea9f 100644
--- a/tempest/lib/services/volume/v1/volumes_client.py
+++ b/tempest/lib/services/volume/v1/volumes_client.py
@@ -38,6 +38,11 @@
"""List all the volumes created.
Params can be a string (must be urlencoded) or a dictionary.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/block-storage/v2/#list-volumes
+ https://developer.openstack.org/api-ref/block-storage/v2/#list-volumes-with-details
"""
url = 'volumes'
if detail:
@@ -63,7 +68,7 @@
For a full list of available parameters, please refer to the official
API reference:
- http://developer.openstack.org/api-ref/block-storage/v1/#create-volume
+ https://developer.openstack.org/api-ref/block-storage/v2/#create-volume
"""
post_body = json.dumps({'volume': kwargs})
resp, body = self.post('volumes', post_body)
@@ -76,7 +81,7 @@
For a full list of available parameters, please refer to the official
API reference:
- http://developer.openstack.org/api-ref/block-storage/v1/#update-volume
+ https://developer.openstack.org/api-ref/block-storage/v2/#update-volume
"""
put_body = json.dumps({'volume': kwargs})
resp, body = self.put('volumes/%s' % volume_id, put_body)
@@ -104,7 +109,7 @@
For a full list of available parameters, please refer to the official
API reference:
- http://developer.openstack.org/api-ref/block-storage/v1/#attach-volume
+ https://developer.openstack.org/api-ref/block-storage/v2/#attach-volume-to-server
"""
post_body = json.dumps({'os-attach': kwargs})
url = 'volumes/%s/action' % (volume_id)
@@ -161,7 +166,7 @@
For a full list of available parameters, please refer to the official
API reference:
- http://developer.openstack.org/api-ref/block-storage/v1/#extend-volume
+ https://developer.openstack.org/api-ref/block-storage/v2/#extend-volume-size
"""
post_body = json.dumps({'os-extend': kwargs})
url = 'volumes/%s/action' % (volume_id)
@@ -174,7 +179,7 @@
For a full list of available parameters, please refer to the official
API reference:
- http://developer.openstack.org/api-ref/block-storage/v1/#reset-volume-status
+ https://developer.openstack.org/api-ref/block-storage/v2/#reset-volume-statuses
"""
post_body = json.dumps({'os-reset_status': kwargs})
resp, body = self.post('volumes/%s/action' % volume_id, post_body)
@@ -186,7 +191,7 @@
For a full list of available parameters, please refer to the official
API reference:
- http://developer.openstack.org/api-ref/block-storage/v1/#create-volume-transfer
+ https://developer.openstack.org/api-ref/block-storage/v2/#create-volume-transfer
"""
post_body = json.dumps({'transfer': kwargs})
resp, body = self.post('os-volume-transfer', post_body)
@@ -207,7 +212,7 @@
For a full list of available parameters, please refer to the official
API reference:
- http://developer.openstack.org/api-ref/block-storage/v1/#list-volume-transfers
+ https://developer.openstack.org/api-ref/block-storage/v2/#list-volume-transfers
"""
url = 'os-volume-transfer'
if params:
@@ -228,7 +233,7 @@
For a full list of available parameters, please refer to the official
API reference:
- http://developer.openstack.org/api-ref/block-storage/v1/#accept-volume-transfer
+ https://developer.openstack.org/api-ref/block-storage/v2/#accept-volume-transfer
"""
url = 'os-volume-transfer/%s/accept' % transfer_id
post_body = json.dumps({'accept': kwargs})
diff --git a/tempest/lib/services/volume/v2/encryption_types_client.py b/tempest/lib/services/volume/v2/encryption_types_client.py
old mode 100755
new mode 100644
diff --git a/tempest/lib/services/volume/v2/volumes_client.py b/tempest/lib/services/volume/v2/volumes_client.py
index d13e449..da3f2b5 100644
--- a/tempest/lib/services/volume/v2/volumes_client.py
+++ b/tempest/lib/services/volume/v2/volumes_client.py
@@ -13,8 +13,6 @@
# License for the specific language governing permissions and limitations
# under the License.
-from debtcollector import moves
-from debtcollector import removals
from oslo_serialization import jsonutils as json
import six
from six.moves.urllib import parse as urllib
@@ -22,43 +20,12 @@
from tempest.lib.common import rest_client
from tempest.lib import exceptions as lib_exc
from tempest.lib.services.volume import base_client
-from tempest.lib.services.volume.v2 import transfers_client
class VolumesClient(base_client.BaseClient):
"""Client class to send CRUD Volume V2 API requests"""
api_version = "v2"
- create_volume_transfer = moves.moved_function(
- transfers_client.TransfersClient.create_volume_transfer,
- 'VolumesClient.create_volume_transfer', __name__,
- message='Use create_volume_transfer from new location.',
- version='Pike', removal_version='Queens')
-
- show_volume_transfer = moves.moved_function(
- transfers_client.TransfersClient.show_volume_transfer,
- 'VolumesClient.show_volume_transfer', __name__,
- message='Use show_volume_transfer from new location.',
- version='Pike', removal_version='Queens')
-
- list_volume_transfers = moves.moved_function(
- transfers_client.TransfersClient.list_volume_transfers,
- 'VolumesClient.list_volume_transfers', __name__,
- message='Use list_volume_transfer from new location.',
- version='Pike', removal_version='Queens')
-
- delete_volume_transfer = moves.moved_function(
- transfers_client.TransfersClient.delete_volume_transfer,
- 'VolumesClient.delete_volume_transfer', __name__,
- message='Use delete_volume_transfer from new location.',
- version='Pike', removal_version='Queens')
-
- accept_volume_transfer = moves.moved_function(
- transfers_client.TransfersClient.accept_volume_transfer,
- 'VolumesClient.accept_volume_transfer', __name__,
- message='Use accept_volume_transfer from new location.',
- version='Pike', removal_version='Queens')
-
def _prepare_params(self, params):
"""Prepares params for use in get or _ext_get methods.
@@ -372,34 +339,6 @@
self.expected_success(200, resp.status)
return rest_client.ResponseBody(resp, body)
- @removals.remove(message="use list_pools from tempest.lib.services."
- "volume.v2.scheduler_stats_client")
- def show_pools(self, detail=False):
- # List all the volumes pools (hosts)
- url = 'scheduler-stats/get_pools'
- if detail:
- url += '?detail=True'
-
- resp, body = self.get(url)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return rest_client.ResponseBody(resp, body)
-
- @removals.remove(message="use show_backend_capabilities from tempest.lib."
- "services.volume.v2.capabilities_client")
- def show_backend_capabilities(self, host):
- """Shows capabilities for a storage back end.
-
- For a full list of available parameters, please refer to the official
- API reference:
- http://developer.openstack.org/api-ref/block-storage/v2/#show-back-end-capabilities
- """
- url = 'capabilities/%s' % host
- resp, body = self.get(url)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return rest_client.ResponseBody(resp, body)
-
def unmanage_volume(self, volume_id):
"""Unmanage volume.
diff --git a/tempest/lib/services/volume/v3/group_snapshots_client.py b/tempest/lib/services/volume/v3/group_snapshots_client.py
index e644f02..6e53e3e 100644
--- a/tempest/lib/services/volume/v3/group_snapshots_client.py
+++ b/tempest/lib/services/volume/v3/group_snapshots_client.py
@@ -60,7 +60,7 @@
self.expected_success(200, resp.status)
return rest_client.ResponseBody(resp, body)
- def list_group_snapshots(self, **params):
+ def list_group_snapshots(self, detail=False, **params):
"""Information for all the tenant's group snapshots.
For more information, please refer to the official API reference:
@@ -68,6 +68,8 @@
https://developer.openstack.org/api-ref/block-storage/v3/#list-group-snapshots-with-details
"""
url = "group_snapshots"
+ if detail:
+ url += "/detail"
if params:
url += '?%s' % urllib.urlencode(params)
resp, body = self.get(url)
@@ -75,6 +77,18 @@
self.expected_success(200, resp.status)
return rest_client.ResponseBody(resp, body)
+ def reset_group_snapshot_status(self, group_snapshot_id, status_to_set):
+ """Resets group snapshot status.
+
+ For more information, please refer to the official API reference:
+ https://developer.openstack.org/api-ref/block-storage/v3/#reset-group-snapshot-status
+ """
+ post_body = json.dumps({'reset_status': {'status': status_to_set}})
+ resp, body = self.post('group_snapshots/%s/action' % group_snapshot_id,
+ post_body)
+ self.expected_success(202, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
def is_resource_deleted(self, id):
try:
self.show_group_snapshot(id)
diff --git a/tempest/lib/services/volume/v3/groups_client.py b/tempest/lib/services/volume/v3/groups_client.py
index b463fdf..e2e477d 100644
--- a/tempest/lib/services/volume/v3/groups_client.py
+++ b/tempest/lib/services/volume/v3/groups_client.py
@@ -109,6 +109,17 @@
self.expected_success(202, resp.status)
return rest_client.ResponseBody(resp, body)
+ def reset_group_status(self, group_id, status_to_set):
+ """Resets group status.
+
+ For more information, please refer to the official API reference:
+ https://developer.openstack.org/api-ref/block-storage/v3/#reset-group-status
+ """
+ post_body = json.dumps({'reset_status': {'status': status_to_set}})
+ resp, body = self.post('groups/%s/action' % group_id, post_body)
+ self.expected_success(202, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
def is_resource_deleted(self, id):
try:
self.show_group(id)
diff --git a/tempest/scenario/README.rst b/tempest/scenario/README.rst
index c1dcccc..efcd139 100644
--- a/tempest/scenario/README.rst
+++ b/tempest/scenario/README.rst
@@ -14,11 +14,12 @@
Any scenario test should have a real-life use case. An example would be:
- - "As operator I want to start with a blank environment":
- 1. upload a glance image
- 2. deploy a vm from it
- 3. ssh to the guest
- 4. create a snapshot of the vm
+- "As operator I want to start with a blank environment":
+
+ 1. upload a glance image
+ 2. deploy a vm from it
+ 3. ssh to the guest
+ 4. create a snapshot of the vm
Why are these tests in Tempest?
diff --git a/tempest/scenario/manager.py b/tempest/scenario/manager.py
index 2843222..06b4b59 100644
--- a/tempest/scenario/manager.py
+++ b/tempest/scenario/manager.py
@@ -89,16 +89,14 @@
# The create_[resource] functions only return body and discard the
# resp part which is not used in scenario tests
- def _create_port(self, network_id, client=None, namestart='port-quotatest',
- **kwargs):
+ def create_port(self, network_id, client=None, **kwargs):
if not client:
client = self.ports_client
- name = data_utils.rand_name(namestart)
+ name = data_utils.rand_name(self.__class__.__name__)
result = client.create_port(
name=name,
network_id=network_id,
**kwargs)
- self.assertIsNotNone(result, 'Unable to allocate port')
port = result['port']
self.addCleanup(test_utils.call_and_ignore_notfound_exc,
client.delete_port, port['id'])
@@ -147,8 +145,7 @@
if vnic_type:
ports = []
- create_port_body = {'binding:vnic_type': vnic_type,
- 'namestart': 'port-smoke'}
+ create_port_body = {'binding:vnic_type': vnic_type}
if kwargs:
# Convert security group names to security group ids
# to pass to create_port
@@ -185,9 +182,9 @@
for net in networks:
net_id = net.get('uuid', net.get('id'))
if 'port' not in net:
- port = self._create_port(network_id=net_id,
- client=clients.ports_client,
- **create_port_body)
+ port = self.create_port(network_id=net_id,
+ client=clients.ports_client,
+ **create_port_body)
ports.append({'port': port['id']})
else:
ports.append({'port': net['port']})
@@ -271,10 +268,8 @@
if backend_name:
extra_specs = {"volume_backend_name": backend_name}
- body = client.create_volume_type(name=randomized_name,
- extra_specs=extra_specs)
- volume_type = body['volume_type']
- self.assertIn('id', volume_type)
+ volume_type = client.create_volume_type(
+ name=randomized_name, extra_specs=extra_specs)['volume_type']
self.addCleanup(client.delete_volume_type, volume_type['id'])
return volume_type
@@ -506,27 +501,6 @@
waiters.wait_for_volume_resource_status(self.volumes_client,
volume['id'], 'available')
- volume = self.volumes_client.show_volume(volume['id'])['volume']
- self.assertEqual('available', volume['status'])
-
- def rebuild_server(self, server_id, image=None,
- preserve_ephemeral=False, wait=True,
- rebuild_kwargs=None):
- if image is None:
- image = CONF.compute.image_ref
-
- rebuild_kwargs = rebuild_kwargs or {}
-
- LOG.debug("Rebuilding server (id: %s, image: %s, preserve eph: %s)",
- server_id, image, preserve_ephemeral)
- self.servers_client.rebuild_server(
- server_id=server_id, image_ref=image,
- preserve_ephemeral=preserve_ephemeral,
- **rebuild_kwargs)
- if wait:
- waiters.wait_for_server_status(self.servers_client,
- server_id, 'ACTIVE')
-
def ping_ip_address(self, ip_address, should_succeed=True,
ping_timeout=None, mtu=None):
timeout = ping_timeout or CONF.validation.ping_timeout
@@ -675,9 +649,7 @@
addresses = server['addresses'][
CONF.validation.network_for_ssh]
else:
- creds_provider = self._get_credentials_provider()
- net_creds = creds_provider.get_primary_creds()
- network = getattr(net_creds, 'network', None)
+ network = self.get_tenant_network()
addresses = (server['addresses'][network['name']]
if network else [])
for address in addresses:
@@ -732,17 +704,14 @@
network['id'])
return network
- def _create_subnet(self, network, subnets_client=None,
- routers_client=None, namestart='subnet-smoke',
- **kwargs):
+ def create_subnet(self, network, subnets_client=None,
+ namestart='subnet-smoke', **kwargs):
"""Create a subnet for the given network
within the cidr block configured for tenant networks.
"""
if not subnets_client:
subnets_client = self.subnets_client
- if not routers_client:
- routers_client = self.routers_client
def cidr_in_use(cidr, tenant_id):
"""Check cidr existence
@@ -858,22 +827,6 @@
floating_ip['id'])
return floating_ip
- def _associate_floating_ip(self, floating_ip, server):
- port_id, _ = self._get_server_port_id_and_ip4(server)
- kwargs = dict(port_id=port_id)
- floating_ip = self.floating_ips_client.update_floatingip(
- floating_ip['id'], **kwargs)['floatingip']
- self.assertEqual(port_id, floating_ip['port_id'])
- return floating_ip
-
- def _disassociate_floating_ip(self, floating_ip):
- """:param floating_ip: floating_ips_client.create_floatingip"""
- kwargs = dict(port_id=None)
- floating_ip = self.floating_ips_client.update_floatingip(
- floating_ip['id'], **kwargs)['floatingip']
- self.assertIsNone(floating_ip['port_id'])
- return floating_ip
-
def check_floating_ip_status(self, floating_ip, status):
"""Verifies floatingip reaches the given status
@@ -901,11 +854,11 @@
LOG.info("FloatingIP: {fp} is at status: {st}"
.format(fp=floating_ip, st=status))
- def _check_tenant_network_connectivity(self, server,
- username,
- private_key,
- should_connect=True,
- servers_for_debug=None):
+ def check_tenant_network_connectivity(self, server,
+ username,
+ private_key,
+ should_connect=True,
+ servers_for_debug=None):
if not CONF.network.project_networks_reachable:
msg = 'Tenant networks not configured to be reachable.'
LOG.info(msg)
@@ -925,16 +878,13 @@
self._log_net_info(e)
raise
- def _check_remote_connectivity(self, source, dest, should_succeed=True,
- nic=None):
+ def check_remote_connectivity(self, source, dest, should_succeed=True,
+ nic=None):
"""assert ping server via source ssh connection
- Note: This is an internal method. Use check_remote_connectivity
- instead.
-
:param source: RemoteClient: an ssh connection from which to ping
- :param dest: and IP to ping against
- :param should_succeed: boolean should ping succeed or not
+ :param dest: an IP to ping against
+ :param should_succeed: boolean: should ping succeed or not
:param nic: specific network interface to ping from
"""
def ping_remote():
@@ -946,28 +896,19 @@
return not should_succeed
return should_succeed
- return test_utils.call_until_true(ping_remote,
- CONF.validation.ping_timeout,
- 1)
+ result = test_utils.call_until_true(ping_remote,
+ CONF.validation.ping_timeout, 1)
+ if result:
+ return
- def check_remote_connectivity(self, source, dest, should_succeed=True,
- nic=None):
- """assert ping server via source ssh connection
-
- :param source: RemoteClient: an ssh connection from which to ping
- :param dest: and IP to ping against
- :param should_succeed: boolean should ping succeed or not
- :param nic: specific network interface to ping from
- """
- result = self._check_remote_connectivity(source, dest, should_succeed,
- nic)
source_host = source.ssh_client.host
if should_succeed:
msg = "Timed out waiting for %s to become reachable from %s" \
% (dest, source_host)
else:
msg = "%s is reachable from %s" % (dest, source_host)
- self.assertTrue(result, msg)
+ self._log_console_output()
+ self.fail(msg)
def _create_security_group(self, security_group_rules_client=None,
tenant_id=None,
@@ -1024,23 +965,6 @@
client.delete_security_group, secgroup['id'])
return secgroup
- def _default_security_group(self, client=None, tenant_id=None):
- """Get default secgroup for given tenant_id.
-
- :returns: default secgroup for given tenant
- """
- if client is None:
- client = self.security_groups_client
- if not tenant_id:
- tenant_id = client.tenant_id
- sgs = [
- sg for sg in list(client.list_security_groups().values())[0]
- if sg['tenant_id'] == tenant_id and sg['name'] == 'default'
- ]
- msg = "No default security group for tenant %s." % (tenant_id)
- self.assertNotEmpty(sgs, msg)
- return sgs[0]
-
def _create_security_group_rule(self, secgroup=None,
sec_group_rules_client=None,
tenant_id=None,
@@ -1069,8 +993,12 @@
if not tenant_id:
tenant_id = security_groups_client.tenant_id
if secgroup is None:
- secgroup = self._default_security_group(
- client=security_groups_client, tenant_id=tenant_id)
+ # Get default secgroup for tenant_id
+ default_secgroups = security_groups_client.list_security_groups(
+ name='default', tenant_id=tenant_id)['security_groups']
+ msg = "No default security group for tenant %s." % (tenant_id)
+ self.assertNotEmpty(default_secgroups, msg)
+ secgroup = default_secgroups[0]
ruleset = dict(security_group_id=secgroup['id'],
tenant_id=secgroup['tenant_id'])
@@ -1158,37 +1086,18 @@
body = client.show_router(router_id)
return body['router']
elif network_id:
- router = self._create_router(client, tenant_id)
- kwargs = {'external_gateway_info': dict(network_id=network_id)}
- router = client.update_router(router['id'], **kwargs)['router']
+ router = client.create_router(
+ name=data_utils.rand_name(self.__class__.__name__ + '-router'),
+ admin_state_up=True,
+ tenant_id=tenant_id,
+ external_gateway_info=dict(network_id=network_id))['router']
+ self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+ client.delete_router, router['id'])
return router
else:
raise Exception("Neither of 'public_router_id' or "
"'public_network_id' has been defined.")
- def _create_router(self, client=None, tenant_id=None,
- namestart='router-smoke'):
- if not client:
- client = self.routers_client
- if not tenant_id:
- tenant_id = client.tenant_id
- name = data_utils.rand_name(namestart)
- result = client.create_router(name=name,
- admin_state_up=True,
- tenant_id=tenant_id)
- router = result['router']
- self.assertEqual(router['name'], name)
- self.addCleanup(test_utils.call_and_ignore_notfound_exc,
- client.delete_router,
- router['id'])
- return router
-
- def _update_router_admin_state(self, router, admin_state_up):
- kwargs = dict(admin_state_up=admin_state_up)
- router = self.routers_client.update_router(
- router['id'], **kwargs)['router']
- self.assertEqual(admin_state_up, router['admin_state_up'])
-
def create_networks(self, networks_client=None,
routers_client=None, subnets_client=None,
tenant_id=None, dns_nameservers=None,
@@ -1223,12 +1132,11 @@
router = self._get_router(client=routers_client,
tenant_id=tenant_id)
subnet_kwargs = dict(network=network,
- subnets_client=subnets_client,
- routers_client=routers_client)
+ subnets_client=subnets_client)
# use explicit check because empty list is a valid option
if dns_nameservers is not None:
subnet_kwargs['dns_nameservers'] = dns_nameservers
- subnet = self._create_subnet(**subnet_kwargs)
+ subnet = self.create_subnet(**subnet_kwargs)
if not routers_client:
routers_client = self.routers_client
router_id = router['id']
@@ -1318,7 +1226,7 @@
def create_container(self, container_name=None):
name = container_name or data_utils.rand_name(
'swift-scenario-container')
- self.container_client.create_container(name)
+ self.container_client.update_container(name)
# look for the container to assure it is created
self.list_and_check_container_objects(name)
LOG.debug('Container %s created', name)
@@ -1355,7 +1263,7 @@
present_obj = []
if not_present_obj is None:
not_present_obj = []
- _, object_list = self.container_client.list_container_contents(
+ _, object_list = self.container_client.list_container_objects(
container_name)
if present_obj:
for obj in present_obj:
@@ -1364,14 +1272,6 @@
for obj in not_present_obj:
self.assertNotIn(obj, object_list)
- def change_container_acl(self, container_name, acl):
- metadata_param = {'metadata_prefix': 'x-container-',
- 'metadata': {'read': acl}}
- self.container_client.update_container_metadata(container_name,
- **metadata_param)
- resp, _ = self.container_client.list_container_metadata(container_name)
- self.assertEqual(resp['x-container-read'], acl)
-
def download_and_verify(self, container_name, obj_name, expected_data):
_, obj = self.object_client.get_object(container_name, obj_name)
self.assertEqual(obj, expected_data)
diff --git a/tempest/scenario/test_network_advanced_server_ops.py b/tempest/scenario/test_network_advanced_server_ops.py
index 340c3c9..7c404ad 100644
--- a/tempest/scenario/test_network_advanced_server_ops.py
+++ b/tempest/scenario/test_network_advanced_server_ops.py
@@ -83,7 +83,7 @@
should_connect=True):
username = CONF.validation.image_ssh_user
private_key = keypair['private_key']
- self._check_tenant_network_connectivity(
+ self.check_tenant_network_connectivity(
server, username, private_key,
should_connect=should_connect,
servers_for_debug=[server])
diff --git a/tempest/scenario/test_network_basic_ops.py b/tempest/scenario/test_network_basic_ops.py
index 2d38b06..ff8837f 100644
--- a/tempest/scenario/test_network_basic_ops.py
+++ b/tempest/scenario/test_network_basic_ops.py
@@ -113,11 +113,16 @@
port_id = None
if boot_with_port:
# create a port on the network and boot with that
- port_id = self._create_port(self.network['id'])['id']
+ port_id = self.create_port(self.network['id'])['id']
self.ports.append({'port': port_id})
server = self._create_server(self.network, port_id)
- self._check_tenant_network_connectivity()
+ ssh_login = CONF.validation.image_ssh_user
+ for server in self.servers:
+ # call the common method in the parent class
+ self.check_tenant_network_connectivity(
+ server, ssh_login, self._get_server_key(server),
+ servers_for_debug=self.servers)
floating_ip = self.create_floating_ip(server)
self.floating_ip_tuple = Floating_IP_tuple(floating_ip, server)
@@ -170,15 +175,6 @@
def _get_server_key(self, server):
return self.keypairs[server['key_name']]['private_key']
- def _check_tenant_network_connectivity(self):
- ssh_login = CONF.validation.image_ssh_user
- for server in self.servers:
- # call the common method in the parent class
- super(TestNetworkBasicOps, self).\
- _check_tenant_network_connectivity(
- server, ssh_login, self._get_server_key(server),
- servers_for_debug=self.servers)
-
def check_public_network_connectivity(
self, should_connect=True, msg=None,
should_check_floating_ip_status=True, mtu=None):
@@ -213,25 +209,28 @@
def _disassociate_floating_ips(self):
floating_ip, _ = self.floating_ip_tuple
- self._disassociate_floating_ip(floating_ip)
- self.floating_ip_tuple = Floating_IP_tuple(
- floating_ip, None)
+ floating_ip = self.floating_ips_client.update_floatingip(
+ floating_ip['id'], port_id=None)['floatingip']
+ self.assertIsNone(floating_ip['port_id'])
+ self.floating_ip_tuple = Floating_IP_tuple(floating_ip, None)
def _reassociate_floating_ips(self):
floating_ip, server = self.floating_ip_tuple
# create a new server for the floating ip
server = self._create_server(self.network)
- self._associate_floating_ip(floating_ip, server)
- self.floating_ip_tuple = Floating_IP_tuple(
- floating_ip, server)
+ port_id, _ = self._get_server_port_id_and_ip4(server)
+ floating_ip = self.floating_ips_client.update_floatingip(
+ floating_ip['id'], port_id=port_id)['floatingip']
+ self.assertEqual(port_id, floating_ip['port_id'])
+ self.floating_ip_tuple = Floating_IP_tuple(floating_ip, server)
def _create_new_network(self, create_gateway=False):
self.new_net = self._create_network()
if create_gateway:
- self.new_subnet = self._create_subnet(
+ self.new_subnet = self.create_subnet(
network=self.new_net)
else:
- self.new_subnet = self._create_subnet(
+ self.new_subnet = self.create_subnet(
network=self.new_net,
gateway_ip=None)
@@ -355,6 +354,12 @@
self.check_remote_connectivity(ssh_source, remote_ip,
should_connect)
+ def _update_router_admin_state(self, router, admin_state_up):
+ kwargs = dict(admin_state_up=admin_state_up)
+ router = self.routers_client.update_router(
+ router['id'], **kwargs)['router']
+ self.assertEqual(admin_state_up, router['admin_state_up'])
+
@decorators.attr(type='smoke')
@decorators.idempotent_id('f323b3ba-82f8-4db7-8ea6-6a895869ec49')
@utils.services('compute', 'network')
@@ -416,6 +421,10 @@
def test_mtu_sized_frames(self):
"""Validate that network MTU sized frames fit through."""
self._setup_network_and_servers()
+ # first check that connectivity works in general for the instance
+ self.check_public_network_connectivity(should_connect=True)
+ # now that we checked general connectivity, test that full size frames
+ # can also pass between nodes
self.check_public_network_connectivity(
should_connect=True, mtu=self.network['mtu'])
diff --git a/tempest/scenario/test_network_v6.py b/tempest/scenario/test_network_v6.py
index b687aa0..9f4e62b 100644
--- a/tempest/scenario/test_network_v6.py
+++ b/tempest/scenario/test_network_v6.py
@@ -12,8 +12,6 @@
# 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 functools
-
from tempest.common import utils
from tempest import config
from tempest.lib.common.utils import test_utils
@@ -78,9 +76,9 @@
if dualnet:
network_v6 = self._create_network()
- sub4 = self._create_subnet(network=network,
- namestart='sub4',
- ip_version=4)
+ sub4 = self.create_subnet(network=network,
+ namestart='sub4',
+ ip_version=4)
router = self._get_router()
self.routers_client.add_router_interface(router['id'],
@@ -93,11 +91,11 @@
self.subnets_v6 = []
for _ in range(n_subnets6):
net6 = network_v6 if dualnet else network
- sub6 = self._create_subnet(network=net6,
- namestart='sub6',
- ip_version=6,
- ipv6_ra_mode=address6_mode,
- ipv6_address_mode=address6_mode)
+ sub6 = self.create_subnet(network=net6,
+ namestart='sub6',
+ ip_version=6,
+ ipv6_ra_mode=address6_mode,
+ ipv6_address_mode=address6_mode)
self.routers_client.add_router_interface(router['id'],
subnet_id=sub6['id'])
@@ -132,7 +130,7 @@
ssh = self.get_remote_client(
ip_address=fip['floating_ip_address'],
username=username, server=srv)
- return ssh, ips, srv["id"]
+ return ssh, ips, srv
def turn_nic6_on(self, ssh, sid, network_id):
"""Turns the IPv6 vNIC on
@@ -163,8 +161,8 @@
n_subnets6=n_subnets6,
dualnet=dualnet)
- sshv4_1, ips_from_api_1, sid1 = self.prepare_server(networks=net_list)
- sshv4_2, ips_from_api_2, sid2 = self.prepare_server(networks=net_list)
+ sshv4_1, ips_from_api_1, srv1 = self.prepare_server(networks=net_list)
+ sshv4_2, ips_from_api_2, srv2 = self.prepare_server(networks=net_list)
def guest_has_address(ssh, addr):
return addr in ssh.exec_command("ip address")
@@ -172,8 +170,8 @@
# Turn on 2nd NIC for Cirros when dualnet
if dualnet:
_, network_v6 = net_list
- self.turn_nic6_on(sshv4_1, sid1, network_v6['id'])
- self.turn_nic6_on(sshv4_2, sid2, network_v6['id'])
+ self.turn_nic6_on(sshv4_1, srv1['id'], network_v6['id'])
+ self.turn_nic6_on(sshv4_2, srv2['id'], network_v6['id'])
# get addresses assigned to vNIC as reported by 'ip address' utility
ips_from_ip_1 = sshv4_1.exec_command("ip address")
@@ -183,17 +181,19 @@
for i in range(n_subnets6):
# v6 should be configured since the image supports it
# It can take time for ipv6 automatic address to get assigned
- srv1_v6_addr_assigned = functools.partial(
- guest_has_address, sshv4_1, ips_from_api_1['6'][i])
-
- srv2_v6_addr_assigned = functools.partial(
- guest_has_address, sshv4_2, ips_from_api_2['6'][i])
-
- self.assertTrue(test_utils.call_until_true(srv1_v6_addr_assigned,
- CONF.validation.ping_timeout, 1))
-
- self.assertTrue(test_utils.call_until_true(srv2_v6_addr_assigned,
- CONF.validation.ping_timeout, 1))
+ for srv, ssh, ips in (
+ (srv1, sshv4_1, ips_from_api_1),
+ (srv2, sshv4_2, ips_from_api_2)):
+ ip = ips['6'][i]
+ result = test_utils.call_until_true(
+ guest_has_address,
+ CONF.validation.ping_timeout, 1, ssh, ip)
+ if not result:
+ self._log_console_output(servers=[srv])
+ self.fail(
+ 'Address %s not configured for instance %s, '
+ 'ip address output is\n%s' %
+ (ip, srv['id'], ssh.exec_command("ip address")))
self.check_remote_connectivity(sshv4_1, ips_from_api_2['4'])
self.check_remote_connectivity(sshv4_2, ips_from_api_1['4'])
diff --git a/tempest/scenario/test_object_storage_basic_ops.py b/tempest/scenario/test_object_storage_basic_ops.py
index da0b1e8..cbe321e 100644
--- a/tempest/scenario/test_object_storage_basic_ops.py
+++ b/tempest/scenario/test_object_storage_basic_ops.py
@@ -58,12 +58,18 @@
5. Delete the object and container
"""
container_name = self.create_container()
- obj_name, _ = self.upload_object_to_container(container_name)
+ obj_name, obj_data = self.upload_object_to_container(container_name)
obj_url = '%s/%s/%s' % (self.object_client.base_url,
container_name, obj_name)
resp, _ = self.object_client.raw_request(obj_url, 'GET')
self.assertEqual(resp.status, 401)
-
- self.change_container_acl(container_name, '.r:*')
- resp, _ = self.object_client.raw_request(obj_url, 'GET')
+ metadata_param = {'X-Container-Read': '.r:*'}
+ self.container_client.create_update_or_delete_container_metadata(
+ container_name, create_update_metadata=metadata_param,
+ create_update_metadata_prefix='')
+ resp, _ = self.container_client.list_container_metadata(container_name)
+ self.assertEqual(metadata_param['X-Container-Read'],
+ resp['x-container-read'])
+ resp, data = self.object_client.raw_request(obj_url, 'GET')
self.assertEqual(resp.status, 200)
+ self.assertEqual(obj_data, data)
diff --git a/tempest/scenario/test_server_advanced_ops.py b/tempest/scenario/test_server_advanced_ops.py
index d4f29ad..89b9fdd 100644
--- a/tempest/scenario/test_server_advanced_ops.py
+++ b/tempest/scenario/test_server_advanced_ops.py
@@ -42,28 +42,6 @@
super(TestServerAdvancedOps, cls).setup_credentials()
@decorators.attr(type='slow')
- @decorators.idempotent_id('e6c28180-7454-4b59-b188-0257af08a63b')
- @testtools.skipUnless(CONF.compute_feature_enabled.resize,
- 'Resize is not available.')
- @utils.services('compute', 'volume')
- def test_resize_volume_backed_server_confirm(self):
- # We create an instance for use in this test
- instance = self.create_server(volume_backed=True)
- instance_id = instance['id']
- resize_flavor = CONF.compute.flavor_ref_alt
- LOG.debug("Resizing instance %s from flavor %s to flavor %s",
- instance['id'], instance['flavor']['id'], resize_flavor)
- self.servers_client.resize_server(instance_id, resize_flavor)
- waiters.wait_for_server_status(self.servers_client, instance_id,
- 'VERIFY_RESIZE')
-
- LOG.debug("Confirming resize of instance %s", instance_id)
- self.servers_client.confirm_resize_server(instance_id)
-
- waiters.wait_for_server_status(self.servers_client, instance_id,
- 'ACTIVE')
-
- @decorators.attr(type='slow')
@decorators.idempotent_id('949da7d5-72c8-4808-8802-e3d70df98e2c')
@testtools.skipUnless(CONF.compute_feature_enabled.suspend,
'Suspend is not available.')
diff --git a/tempest/scenario/test_volume_boot_pattern.py b/tempest/scenario/test_volume_boot_pattern.py
index 64ea8f6..beb039c 100644
--- a/tempest/scenario/test_volume_boot_pattern.py
+++ b/tempest/scenario/test_volume_boot_pattern.py
@@ -208,7 +208,19 @@
# boot instance from EBS image
instance = self.create_server(image_id=image['id'])
- # just ensure that instance booted
+
+ # Verify the server was created from the image
+ created_volume = instance['os-extended-volumes:volumes_attached']
+ self.assertNotEmpty(created_volume, "No volume attachment found.")
+ created_volume_info = self.volumes_client.show_volume(
+ created_volume[0]['id'])['volume']
+ self.assertEqual(instance['id'],
+ created_volume_info['attachments'][0]['server_id'])
+ self.assertEqual(created_volume[0]['id'],
+ created_volume_info['attachments'][0]['volume_id'])
+ self.assertEqual(
+ volume_origin['volume_image_metadata']['image_id'],
+ created_volume_info['volume_image_metadata']['image_id'])
# delete instance
self._delete_server(instance)
diff --git a/tempest/services/object_storage/__init__.py b/tempest/services/object_storage/__init__.py
deleted file mode 100644
index a2f0992..0000000
--- a/tempest/services/object_storage/__init__.py
+++ /dev/null
@@ -1,24 +0,0 @@
-# Copyright (c) 2016 Hewlett-Packard Enterprise Development Company, L.P.
-#
-# 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.object_storage.bulk_middleware_client import \
- BulkMiddlewareClient
-from tempest.lib.services.object_storage.capabilities_client import \
- CapabilitiesClient
-from tempest.services.object_storage.account_client import AccountClient
-from tempest.services.object_storage.container_client import ContainerClient
-from tempest.services.object_storage.object_client import ObjectClient
-
-__all__ = ['AccountClient', 'BulkMiddlewareClient', 'CapabilitiesClient',
- 'ContainerClient', 'ObjectClient']
diff --git a/tempest/services/object_storage/container_client.py b/tempest/services/object_storage/container_client.py
deleted file mode 100644
index afedd36..0000000
--- a/tempest/services/object_storage/container_client.py
+++ /dev/null
@@ -1,150 +0,0 @@
-# Copyright 2012 OpenStack Foundation
-# All Rights Reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may
-# not use this file except in compliance with the License. You may obtain
-# a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations
-# under the License.
-
-from xml.etree import ElementTree as etree
-
-from oslo_serialization import jsonutils as json
-from six.moves.urllib import parse as urllib
-
-from tempest.lib.common import rest_client
-
-
-class ContainerClient(rest_client.RestClient):
-
- def create_container(
- self, container_name,
- metadata=None,
- remove_metadata=None,
- metadata_prefix='X-Container-Meta-',
- remove_metadata_prefix='X-Remove-Container-Meta-'):
- """Creates a container
-
- with optional metadata passed in as a dictionary
- """
- url = str(container_name)
- headers = {}
-
- if metadata is not None:
- for key in metadata:
- headers[metadata_prefix + key] = metadata[key]
- if remove_metadata is not None:
- for key in remove_metadata:
- headers[remove_metadata_prefix + key] = remove_metadata[key]
-
- resp, body = self.put(url, body=None, headers=headers)
- self.expected_success([201, 202], resp.status)
- return resp, body
-
- def delete_container(self, container_name):
- """Deletes the container (if it's empty)."""
- url = str(container_name)
- resp, body = self.delete(url)
- self.expected_success(204, resp.status)
- return resp, body
-
- def update_container_metadata(
- self, container_name,
- metadata=None,
- remove_metadata=None,
- metadata_prefix='X-Container-Meta-',
- remove_metadata_prefix='X-Remove-Container-Meta-'):
- """Updates arbitrary metadata on container."""
- url = str(container_name)
- headers = {}
-
- if metadata is not None:
- for key in metadata:
- headers[metadata_prefix + key] = metadata[key]
- if remove_metadata is not None:
- for key in remove_metadata:
- headers[remove_metadata_prefix + key] = remove_metadata[key]
-
- resp, body = self.post(url, body=None, headers=headers)
- self.expected_success(204, resp.status)
- return resp, body
-
- def delete_container_metadata(self, container_name, metadata,
- metadata_prefix='X-Remove-Container-Meta-'):
- """Deletes arbitrary metadata on container."""
- url = str(container_name)
- headers = {}
-
- if metadata is not None:
- for item in metadata:
- headers[metadata_prefix + item] = metadata[item]
-
- resp, body = self.post(url, body=None, headers=headers)
- self.expected_success(204, resp.status)
- return resp, body
-
- def list_container_metadata(self, container_name):
- """Retrieves container metadata headers"""
- url = str(container_name)
- resp, body = self.head(url)
- self.expected_success(204, resp.status)
- return resp, body
-
- def list_container_contents(self, container, params=None):
- """List the objects in a container, given the container name
-
- Returns the container object listing as a plain text list, or as
- xml or json if that option is specified via the 'format' argument.
-
- Optional Arguments:
- limit = integer
- For an integer value n, limits the number of results to at most
- n values.
-
- marker = 'string'
- Given a string value x, return object names greater in value
- than the specified marker.
-
- prefix = 'string'
- For a string value x, causes the results to be limited to names
- beginning with the substring x.
-
- format = 'json' or 'xml'
- Specify either json or xml to return the respective serialized
- response.
- If json, returns a list of json objects
- if xml, returns a string of xml
-
- path = 'string'
- For a string value x, return the object names nested in the
- pseudo path (assuming preconditions are met - see below).
-
- delimiter = 'character'
- For a character c, return all the object names nested in the
- container (without the need for the directory marker objects).
- """
-
- url = str(container)
- if params:
- url += '?'
- url += '&%s' % urllib.urlencode(params)
-
- resp, body = self.get(url, headers={})
- if params and params.get('format') == 'json':
- body = json.loads(body)
- elif params and params.get('format') == 'xml':
- body = etree.fromstring(body)
- # Else the content-type is plain/text
- else:
- body = [
- obj_name for obj_name in body.decode().split('\n') if obj_name
- ]
-
- self.expected_success([200, 204], resp.status)
- return resp, body
diff --git a/tempest/test.py b/tempest/test.py
index a4cc2cc..9da85d5 100644
--- a/tempest/test.py
+++ b/tempest/test.py
@@ -26,10 +26,9 @@
from tempest import clients
from tempest.common import credentials_factory as credentials
from tempest.common import utils
-import tempest.common.validation_resources as vresources
from tempest import config
-from tempest.lib.common import cred_client
from tempest.lib.common import fixed_network
+from tempest.lib.common import validation_resources as vr
from tempest.lib import decorators
from tempest.lib import exceptions as lib_exc
@@ -98,16 +97,24 @@
- resource_cleanup
"""
- setUpClassCalled = False
-
# NOTE(andreaf) credentials holds a list of the credentials to be allocated
# at class setup time. Credential types can be 'primary', 'alt', 'admin' or
# a list of roles - the first element of the list being a label, and the
# rest the actual roles
credentials = []
+
+ # Track if setUpClass was invoked
+ __setupclass_called = False
+
+ # Network resources to be provisioned for the requested test credentials.
+ # Only used with the dynamic credentials provider.
+ _network_resources = {}
+
+ # Stack of resource cleanups
+ _class_cleanups = []
+
# Resources required to validate a server using ssh
- validation_resources = {}
- network_resources = {}
+ _validation_resources = {}
# NOTE(sdague): log_format is defined inline here instead of using the oslo
# default because going through the config path recouples config to the
@@ -122,23 +129,39 @@
TIMEOUT_SCALING_FACTOR = 1
@classmethod
+ def _reset_class(cls):
+ cls.__setup_credentials_called = False
+ cls.__resource_cleanup_called = False
+ cls.__skip_checks_called = False
+ # Stack of callable to be invoked in reverse order
+ cls._class_cleanups = []
+ # Stack of (name, callable) to be invoked in reverse order at teardown
+ cls._teardowns = []
+
+ @classmethod
def setUpClass(cls):
+ cls.__setupclass_called = True
+ # Reset state
+ cls._reset_class()
# It should never be overridden by descendants
if hasattr(super(BaseTestCase, cls), 'setUpClass'):
super(BaseTestCase, cls).setUpClass()
- cls.setUpClassCalled = True
- # Stack of (name, callable) to be invoked in reverse order at teardown
- cls.teardowns = []
# All the configuration checks that may generate a skip
cls.skip_checks()
+ if not cls.__skip_checks_called:
+ raise RuntimeError("skip_checks for %s did not call the super's "
+ "skip_checks" % cls.__name__)
try:
# Allocation of all required credentials and client managers
- cls.teardowns.append(('credentials', cls.clear_credentials))
+ cls._teardowns.append(('credentials', cls.clear_credentials))
cls.setup_credentials()
+ if not cls.__setup_credentials_called:
+ raise RuntimeError("setup_credentials for %s did not call the "
+ "super's setup_credentials" % cls.__name__)
# Shortcuts to clients
cls.setup_clients()
# Additional class-wide test resources
- cls.teardowns.append(('resources', cls.resource_cleanup))
+ cls._teardowns.append(('resources', cls.resource_cleanup))
cls.resource_setup()
except Exception:
etype, value, trace = sys.exc_info()
@@ -165,18 +188,29 @@
# If there was no exception during setup we shall re-raise the first
# exception in teardown
re_raise = (etype is None)
- while cls.teardowns:
- name, teardown = cls.teardowns.pop()
+ while cls._teardowns:
+ name, teardown = cls._teardowns.pop()
# Catch any exception in tearDown so we can re-raise the original
# exception at the end
try:
teardown()
+ if name == 'resources':
+ if not cls.__resource_cleanup_called:
+ raise RuntimeError(
+ "resource_cleanup for %s did not call the "
+ "super's resource_cleanup" % cls.__name__)
except Exception as te:
sys_exec_info = sys.exc_info()
tetype = sys_exec_info[0]
- # TODO(andreaf): Till we have the ability to cleanup only
- # resources that were successfully setup in resource_cleanup,
- # log AttributeError as info instead of exception.
+ # TODO(andreaf): Resource cleanup is often implemented by
+ # storing an array of resources at class level, and cleaning
+ # them up during `resource_cleanup`.
+ # In case of failure during setup, some resource arrays might
+ # not be defined at all, in which case the cleanup code might
+ # trigger an AttributeError. In such cases we log
+ # AttributeError as info instead of exception. Once all
+ # cleanups are migrated to addClassResourceCleanup we can
+ # remove this.
if tetype is AttributeError and name == 'resources':
LOG.info("tearDownClass of %s failed: %s", name, te)
else:
@@ -212,13 +246,37 @@
"""Class level skip checks.
Subclasses verify in here all conditions that might prevent the
- execution of the entire test class.
- Checks implemented here may not make use API calls, and should rely on
- configuration alone.
- In general skip checks that require an API call are discouraged.
- If one is really needed it may be implemented either in the
- resource_setup or at test level.
+ execution of the entire test class. Skipping here prevents any other
+ class fixture from being executed i.e. no credentials or other
+ resource allocation will happen.
+
+ Tests defined in the test class will no longer appear in test results.
+ The `setUpClass` for the entire test class will be marked as SKIPPED
+ instead.
+
+ At this stage no test credentials are available, so skip checks
+ should rely on configuration alone. This is deliberate since skips
+ based on the result of an API call are discouraged.
+
+ The following checks are implemented in `test.py` already:
+ - check that alt credentials are available when requested by the test
+ - check that admin credentials are available when requested by the test
+ - check that the identity version specified by the test is marked as
+ enabled in the configuration
+
+ Overriders of skip_checks must always invoke skip_check on `super`
+ first.
+
+ Example::
+
+ @classmethod
+ def skip_checks(cls):
+ super(Example, cls).skip_checks()
+ if not CONF.service_available.my_service:
+ skip_msg = ("%s skipped as my_service is not available")
+ raise cls.skipException(skip_msg % cls.__name__)
"""
+ cls.__skip_checks_called = True
identity_version = cls.get_identity_version()
# setting force_tenant_isolation to True also needs admin credentials.
if ('admin' in cls.credentials or
@@ -243,13 +301,67 @@
def setup_credentials(cls):
"""Allocate credentials and create the client managers from them.
- For every element of credentials param function creates tenant/user,
- Then it creates client manager for that credential.
+ `setup_credentials` looks for the content of the `credentials`
+ attribute in the test class. If the value is a non-empty collection,
+ a credentials provider is setup, and credentials are provisioned or
+ allocated based on the content of the collection. Every set of
+ credentials is associated to an object of type `cls.client_manager`.
+ The client manager is accessible by tests via class attribute
+ `os_[type]`:
- Network related tests must override this function with
- set_network_resources() method, otherwise it will create
- network resources(network resources are created in a later step).
+ Valid values in `credentials` are:
+ - 'primary':
+ A normal user is provisioned.
+ It can be used only once. Multiple entries will be ignored.
+ Clients are available at os_primary.
+ - 'alt':
+ A normal user other than 'primary' is provisioned.
+ It can be used only once. Multiple entries will be ignored.
+ Clients are available at os_alt.
+ - 'admin':
+ An admin user is provisioned.
+ It can be used only once. Multiple entries will be ignored.
+ Clients are available at os_admin.
+ - A list in the format ['any_label', 'role1', ... , 'roleN']:
+ A client with roles <list>[1:] is provisioned.
+ It can be used multiple times, with unique labels.
+ Clients are available at os_roles_<list>[0].
+
+ By default network resources are allocated (in case of dynamic
+ credentials). Tests that do not need network or that require a
+ custom network setup must specify which network resources shall
+ be provisioned using the `set_network_resources()` method (note
+ that it must be invoked before the `setup_credentials` is
+ invoked on super).
+
+ Example::
+
+ class TestWithCredentials(test.BaseTestCase):
+
+ credentials = ['primary', 'admin',
+ ['special', 'special_role1']]
+
+ @classmethod
+ def setup_credentials(cls):
+ # set_network_resources must be called first
+ cls.set_network_resources(network=True)
+ super(TestWithCredentials, cls).setup_credentials()
+
+ @classmethod
+ def setup_clients(cls):
+ cls.servers = cls.os_primary.compute.ServersClient()
+ cls.admin_servers = cls.os_admin.compute.ServersClient()
+ # certain API calls may require a user with a specific
+ # role assigned. In this example `special_role1` is
+ # assigned to the user in `cls.os_roles_special`.
+ cls.special_servers = (
+ cls.os_roles_special.compute.ServersClient())
+
+ def test_special_servers(self):
+ # Do something with servers
+ pass
"""
+ cls.__setup_credentials_called = True
for credentials_type in cls.credentials:
# This may raise an exception in case credentials are not available
# In that case we want to let the exception through and the test
@@ -291,54 +403,184 @@
@classmethod
def setup_clients(cls):
- """Create links to the clients into the test object."""
- # TODO(andreaf) There is a fair amount of code that could me moved from
- # base / test classes in here. Ideally tests should be able to only
- # specify which client is `client` and nothing else.
+ """Create aliases to the clients in the client managers.
+
+ `setup_clients` is invoked after the credential provisioning step.
+ Client manager objects are available to tests already. The purpose
+ of this helper is to setup shortcuts to specific clients that are
+ useful for the tests implemented in the test class.
+
+ Its purpose is mostly for code readability, however it should be used
+ carefully to avoid doing exactly the opposite, i.e. making the code
+ unreadable and hard to debug. If aliases are defined in a super class
+ it won't be obvious what they refer to, so it's good practice to define
+ all aliases used in the class. Aliases are meant to be shortcuts to
+ be used in tests, not shortcuts to avoid helper method attributes.
+ If an helper method starts relying on a client alias and a subclass
+ overrides that alias, it will become rather difficult to understand
+ what the helper method actually does.
+
+ Example::
+
+ class TestDoneItRight(test.BaseTestCase):
+
+ credentials = ['primary', 'alt']
+
+ @classmethod
+ def setup_clients(cls):
+ super(TestDoneItRight, cls).setup_clients()
+ cls.servers = cls.os_primary.ServersClient()
+ cls.servers_alt = cls.os_alt.ServersClient()
+
+ def _a_good_helper(self, clients):
+ # Some complex logic we're going to use many times
+ servers = clients.ServersClient()
+ vm = servers.create_server(...)
+
+ def delete_server():
+ test_utils.call_and_ignore_notfound_exc(
+ servers.delete_server, vm['id'])
+
+ self.addCleanup(self.delete_server)
+ return vm
+
+ def test_with_servers(self):
+ vm = self._a_good_helper(os.primary)
+ vm_alt = self._a_good_helper(os.alt)
+ cls.servers.show_server(vm['id'])
+ cls.servers_alt.show_server(vm_alt['id'])
+ """
pass
@classmethod
def resource_setup(cls):
- """Class level resource setup for test cases."""
- if (CONF.validation.ip_version_for_ssh not in (4, 6) and
- CONF.service_available.neutron):
- msg = "Invalid IP version %s in ip_version_for_ssh. Use 4 or 6"
- raise lib_exc.InvalidConfiguration(
- msg % CONF.validation.ip_version_for_ssh)
- if hasattr(cls, "os_primary"):
- vr = cls.validation_resources
- cls.validation_resources = vresources.create_validation_resources(
- cls.os_primary,
- use_neutron=CONF.service_available.neutron,
- ethertype='IPv' + str(CONF.validation.ip_version_for_ssh),
- floating_network_id=CONF.network.public_network_id,
- floating_network_name=CONF.network.floating_network_name,
- **vr)
- else:
- LOG.warning("Client manager not found, validation resources not"
- " created")
+ """Class level resource setup for test cases.
+
+ `resource_setup` is invoked once all credentials (and related network
+ resources have been provisioned and after client aliases - if any -
+ have been defined.
+
+ The use case for `resource_setup` is test optimization: provisioning
+ of project-specific "expensive" resources that are not dirtied by tests
+ and can thus safely be re-used by multiple tests.
+
+ System wide resources shared by all tests could instead be provisioned
+ only once, before the test run.
+
+ Resources provisioned here must be cleaned up during
+ `resource_cleanup`. This is best achieved by scheduling a cleanup via
+ `addClassResourceCleanup`.
+
+ Some test resources have an asynchronous delete process. It's best
+ practice for them to schedule a wait for delete via
+ `addClassResourceCleanup` to avoid having resources in process of
+ deletion when we reach the credentials cleanup step.
+
+ Example::
+
+ @classmethod
+ def resource_setup(cls):
+ super(MyTest, cls).resource_setup()
+ servers = cls.os_primary.compute.ServersClient()
+ # Schedule delete and wait so that we can first delete the
+ # two servers and then wait for both to delete
+ # Create server 1
+ cls.shared_server = servers.create_server()
+ # Create server 2. If something goes wrong we schedule cleanup
+ # of server 1 anyways.
+ try:
+ cls.shared_server2 = servers.create_server()
+ # Wait server 2
+ cls.addClassResourceCleanup(
+ waiters.wait_for_server_termination,
+ servers, cls.shared_server2['id'],
+ ignore_error=False)
+ finally:
+ # Wait server 1
+ cls.addClassResourceCleanup(
+ waiters.wait_for_server_termination,
+ servers, cls.shared_server['id'],
+ ignore_error=False)
+ # Delete server 1
+ cls.addClassResourceCleanup(
+ test_utils.call_and_ignore_notfound_exc,
+ servers.delete_server,
+ cls.shared_server['id'])
+ # Delete server 2 (if it was created)
+ if hasattr(cls, 'shared_server2'):
+ cls.addClassResourceCleanup(
+ test_utils.call_and_ignore_notfound_exc,
+ servers.delete_server,
+ cls.shared_server2['id'])
+ """
+ pass
@classmethod
def resource_cleanup(cls):
"""Class level resource cleanup for test cases.
- Resource cleanup must be able to handle the case of partially setup
- resources, in case a failure during `resource_setup` should happen.
+ Resource cleanup processes the stack of cleanups produced by
+ `addClassResourceCleanup` and then cleans up validation resources
+ if any were provisioned.
+
+ All cleanups are processed whatever the outcome. Exceptions are
+ accumulated and re-raised as a `MultipleExceptions` at the end.
+
+ In most cases test cases won't need to override `resource_cleanup`,
+ but if they do they must invoke `resource_cleanup` on super.
+
+ Example::
+
+ class TestWithReallyComplexCleanup(test.BaseTestCase):
+
+ @classmethod
+ def resource_setup(cls):
+ # provision resource A
+ cls.addClassResourceCleanup(delete_resource, A)
+ # provision resource B
+ cls.addClassResourceCleanup(delete_resource, B)
+
+ @classmethod
+ def resource_cleanup(cls):
+ # It's possible to override resource_cleanup but in most
+ # cases it shouldn't be required. Nothing that may fail
+ # should be executed before the call to super since it
+ # might cause resource leak in case of error.
+ super(TestWithReallyComplexCleanup, cls).resource_cleanup()
+ # At this point test credentials are still available but
+ # anything from the cleanup stack has been already deleted.
"""
- if cls.validation_resources:
- if hasattr(cls, "os_primary"):
- vr = cls.validation_resources
- vresources.clear_validation_resources(
- cls.os_primary,
- use_neutron=CONF.service_available.neutron, **vr)
- cls.validation_resources = {}
- else:
- LOG.warning("Client manager not found, validation resources "
- "not deleted")
+ cls.__resource_cleanup_called = True
+ cleanup_errors = []
+ while cls._class_cleanups:
+ try:
+ fn, args, kwargs = cls._class_cleanups.pop()
+ fn(*args, **kwargs)
+ except Exception:
+ cleanup_errors.append(sys.exc_info())
+ if cleanup_errors:
+ raise testtools.MultipleExceptions(*cleanup_errors)
+
+ @classmethod
+ def addClassResourceCleanup(cls, fn, *arguments, **keywordArguments):
+ """Add a cleanup function to be called during resource_cleanup.
+
+ Functions added with addClassResourceCleanup will be called in reverse
+ order of adding at the beginning of resource_cleanup, before any
+ credential, networking or validation resources cleanup is processed.
+
+ If a function added with addClassResourceCleanup raises an exception,
+ the error will be recorded as a test error, and the next cleanup will
+ then be run.
+
+ Cleanup functions are always called during the test class tearDown
+ fixture, even if an exception occured during setUp or tearDown.
+ """
+ cls._class_cleanups.append((fn, arguments, keywordArguments))
def setUp(self):
super(BaseTestCase, self).setUp()
- if not self.setUpClassCalled:
+ if not self.__setupclass_called:
raise RuntimeError("setUpClass does not calls the super's"
"setUpClass in the "
+ self.__class__.__name__)
@@ -369,37 +611,6 @@
def credentials_provider(self):
return self._get_credentials_provider()
- @property
- def identity_utils(self):
- """A client that abstracts v2 and v3 identity operations.
-
- This can be used for creating and tearing down projects in tests. It
- should not be used for testing identity features.
- """
- if CONF.identity.auth_version == 'v2':
- client = self.os_admin.identity_client
- users_client = self.os_admin.users_client
- project_client = self.os_admin.tenants_client
- roles_client = self.os_admin.roles_client
- domains_client = None
- else:
- client = self.os_admin.identity_v3_client
- users_client = self.os_admin.users_v3_client
- project_client = self.os_admin.projects_client
- roles_client = self.os_admin.roles_v3_client
- domains_client = self.os_admin.domains_client
-
- try:
- domain = client.auth_provider.credentials.project_domain_name
- except AttributeError:
- domain = 'Default'
-
- return cred_client.get_creds_client(client, project_client,
- users_client,
- roles_client,
- domains_client,
- project_domain_name=domain)
-
@classmethod
def get_identity_version(cls):
"""Returns the identity version used by the test class"""
@@ -421,7 +632,7 @@
False)
cls._creds_provider = credentials.get_credentials_provider(
- name=cls.__name__, network_resources=cls.network_resources,
+ name=cls.__name__, network_resources=cls._network_resources,
force_tenant_isolation=force_tenant_isolation)
return cls._creds_provider
@@ -476,62 +687,131 @@
if hasattr(cls, '_creds_provider'):
cls._creds_provider.clear_creds()
+ @staticmethod
+ def _validation_resources_params_from_conf():
+ return dict(
+ keypair=(CONF.validation.auth_method.lower() == "keypair"),
+ floating_ip=(CONF.validation.connect_method.lower() == "floating"),
+ security_group=CONF.validation.security_group,
+ security_group_rules=CONF.validation.security_group_rules,
+ use_neutron=CONF.service_available.neutron,
+ ethertype='IPv' + str(CONF.validation.ip_version_for_ssh),
+ floating_network_id=CONF.network.public_network_id,
+ floating_network_name=CONF.network.floating_network_name)
+
@classmethod
- def set_validation_resources(cls, keypair=None, floating_ip=None,
- security_group=None,
- security_group_rules=None):
- """Specify which ssh server validation resources should be created.
+ def get_class_validation_resources(cls, os_clients):
+ """Provision validation resources according to configuration
- Each of the argument must be set to either None, True or False, with
- None - use default from config (security groups and security group
- rules get created when set to None)
- False - Do not create the validation resource
- True - create the validation resource
+ This is a wrapper around `create_validation_resources` from
+ `tempest.common.validation_resources` that passes parameters from
+ Tempest configuration. Only one instance of class level
+ validation resources is managed by the helper, so If resources
+ were already provisioned before, existing ones will be returned.
- @param keypair
- @param security_group
- @param security_group_rules
- @param floating_ip
+ Resources are returned as a dictionary. They are also scheduled for
+ automatic cleanup during class teardown using
+ `addClassResourcesCleanup`.
+
+ If `CONF.validation.run_validation` is False no resource will be
+ provisioned at all.
+
+ @param os_clients: Clients to be used to provision the resources.
"""
if not CONF.validation.run_validation:
return
- if keypair is None:
- keypair = (CONF.validation.auth_method.lower() == "keypair")
+ if os_clients in cls._validation_resources:
+ return cls._validation_resources[os_clients]
- if floating_ip is None:
- floating_ip = (CONF.validation.connect_method.lower() ==
- "floating")
+ if (CONF.validation.ip_version_for_ssh not in (4, 6) and
+ CONF.service_available.neutron):
+ msg = "Invalid IP version %s in ip_version_for_ssh. Use 4 or 6"
+ raise lib_exc.InvalidConfiguration(
+ msg % CONF.validation.ip_version_for_ssh)
- if security_group is None:
- security_group = CONF.validation.security_group
+ resources = vr.create_validation_resources(
+ os_clients,
+ **cls._validation_resources_params_from_conf())
- if security_group_rules is None:
- security_group_rules = CONF.validation.security_group_rules
+ cls.addClassResourceCleanup(
+ vr.clear_validation_resources, os_clients,
+ use_neutron=CONF.service_available.neutron,
+ **resources)
+ cls._validation_resources[os_clients] = resources
+ return resources
- if not cls.validation_resources:
- cls.validation_resources = {
- 'keypair': keypair,
- 'security_group': security_group,
- 'security_group_rules': security_group_rules,
- 'floating_ip': floating_ip}
+ def get_test_validation_resources(self, os_clients):
+ """Returns a dict of validation resources according to configuration
+
+ Initialise a validation resources fixture based on configuration.
+ Start the fixture and returns the validation resources.
+
+ If `CONF.validation.run_validation` is False no resource will be
+ provisioned at all.
+
+ @param os_clients: Clients to be used to provision the resources.
+ """
+
+ params = {}
+ # Test will try to use the fixture, so for this to be useful
+ # we must return a fixture. If validation is disabled though
+ # we don't need to provision anything, which is the default
+ # behavior for the fixture.
+ if CONF.validation.run_validation:
+ params = self._validation_resources_params_from_conf()
+
+ validation = self.useFixture(
+ vr.ValidationResourcesFixture(os_clients, **params))
+ return validation.resources
@classmethod
def set_network_resources(cls, network=False, router=False, subnet=False,
dhcp=False):
"""Specify which network resources should be created
+ The dynamic credentials provider by default provisions network
+ resources for each user/project that is provisioned. This behavior
+ can be altered using this method, which allows tests to define which
+ specific network resources to be provisioned - none if no parameter
+ is specified.
+
+ This method is designed so that only the network resources set on the
+ leaf class are honoured.
+
+ Credentials are provisioned as part of the class setup fixture,
+ during the `setup_credentials` step. For this to be effective this
+ helper must be invoked before super's `setup_credentials` is executed.
+
@param network
@param router
@param subnet
@param dhcp
+
+ Example::
+
+ @classmethod
+ def setup_credentials(cls):
+ # Do not setup network resources for this test
+ cls.set_network_resources()
+ super(MyTest, cls).setup_credentials()
"""
- # network resources should be set only once from callers
+ # If this is invoked after the credentials are setup, it won't take
+ # any effect. To avoid this situation, fail the test in case this was
+ # invoked too late in the test lifecycle.
+ if cls.__setup_credentials_called:
+ raise RuntimeError(
+ "set_network_resources invoked after setup_credentials on the "
+ "super class has been already invoked. For "
+ "set_network_resources to have effect please invoke it before "
+ "the call to super().setup_credentials")
+
+ # Network resources should be set only once from callers
# in order to ensure that even if it's called multiple times in
# a chain of overloaded methods, the attribute is set only
- # in the leaf class
- if not cls.network_resources:
- cls.network_resources = {
+ # in the leaf class.
+ if not cls._network_resources:
+ cls._network_resources = {
'network': network,
'router': router,
'subnet': subnet,
diff --git a/tempest/test_discover/plugins.py b/tempest/test_discover/plugins.py
index 1206e3f..9c18052 100644
--- a/tempest/test_discover/plugins.py
+++ b/tempest/test_discover/plugins.py
@@ -76,7 +76,7 @@
conf.register_opt(my_config.service_option,
group='service_available')
conf.register_group(my_config.my_service_group)
- conf.register_opts(my_config.MyService +
+ conf.register_opts(my_config.MyServiceGroup,
my_config.my_service_group)
conf.register_group(my_config.my_service_feature_group)
diff --git a/tempest/tests/api/compute/test_base.py b/tempest/tests/api/compute/test_base.py
index 6345728..5024100 100644
--- a/tempest/tests/api/compute/test_base.py
+++ b/tempest/tests/api/compute/test_base.py
@@ -37,14 +37,16 @@
fake_image = mock.Mock(response={'location': image_id})
compute_images_client.create_image.return_value = fake_image
# call the utility method
- image = compute_base.BaseV2ComputeTest.create_image_from_server(
- mock.sentinel.server_id, name='fake-snapshot-name')
+ cleanup_path = 'tempest.test.BaseTestCase.addClassResourceCleanup'
+ with mock.patch(cleanup_path) as mock_cleanup:
+ image = compute_base.BaseV2ComputeTest.create_image_from_server(
+ mock.sentinel.server_id, name='fake-snapshot-name')
self.assertEqual(fake_image, image)
# make our assertions
compute_images_client.create_image.assert_called_once_with(
mock.sentinel.server_id, name='fake-snapshot-name')
- self.assertEqual(1, len(compute_base.BaseV2ComputeTest.images))
- self.assertEqual(image_id, compute_base.BaseV2ComputeTest.images[0])
+ mock_cleanup.assert_called_once()
+ self.assertIn(image_id, mock_cleanup.call_args[0])
@mock.patch.multiple(compute_base.BaseV2ComputeTest,
compute_images_client=mock.DEFAULT,
diff --git a/tempest/tests/cmd/test_account_generator.py b/tempest/tests/cmd/test_account_generator.py
index f907bd0..8bf4c5b 100644
--- a/tempest/tests/cmd/test_account_generator.py
+++ b/tempest/tests/cmd/test_account_generator.py
@@ -44,6 +44,7 @@
self.patchobject(config, 'TempestConfigPrivate',
fake_config.FakePrivate)
self.opts = FakeOpts(version=identity_version)
+ self.patch('oslo_log.log.setup', autospec=True)
def mock_resource_creation(self):
fake_resource = dict(id='id', name='name')
diff --git a/tempest/tests/cmd/test_run.py b/tempest/tests/cmd/test_run.py
index 6e1250f..0485e14 100644
--- a/tempest/tests/cmd/test_run.py
+++ b/tempest/tests/cmd/test_run.py
@@ -40,6 +40,7 @@
setattr(args, "subunit", True)
setattr(args, "parallel", False)
setattr(args, "concurrency", 10)
+ setattr(args, "load_list", '')
options = self.run_cmd._build_options(args)
self.assertEqual(['--subunit',
'--concurrency=10'],
diff --git a/tempest/tests/cmd/test_verify_tempest_config.py b/tempest/tests/cmd/test_verify_tempest_config.py
index 810f9e5..8641b63 100644
--- a/tempest/tests/cmd/test_verify_tempest_config.py
+++ b/tempest/tests/cmd/test_verify_tempest_config.py
@@ -176,22 +176,6 @@
False, True)
@mock.patch('tempest.lib.common.http.ClosingHttp.request')
- def test_verify_keystone_api_versions_no_v2(self, mock_request):
- self.useFixture(fixtures.MockPatchObject(
- verify_tempest_config, '_get_unversioned_endpoint',
- return_value='http://fake_endpoint:5000'))
- fake_resp = {'versions': {'values': [{'id': 'v3.0'}]}}
- fake_resp = json.dumps(fake_resp)
- mock_request.return_value = (None, fake_resp)
- fake_os = mock.MagicMock()
- with mock.patch.object(verify_tempest_config,
- 'print_and_or_update') as print_mock:
- verify_tempest_config.verify_keystone_api_versions(fake_os, True)
- print_mock.assert_called_once_with('api_v2',
- 'identity-feature-enabled',
- False, True)
-
- @mock.patch('tempest.lib.common.http.ClosingHttp.request')
def test_verify_cinder_api_versions_no_v3(self, mock_request):
self.useFixture(fixtures.MockPatchObject(
verify_tempest_config, '_get_unversioned_endpoint',
diff --git a/tempest/tests/common/test_credentials_factory.py b/tempest/tests/common/test_credentials_factory.py
index 020818e..7cf87f8 100644
--- a/tempest/tests/common/test_credentials_factory.py
+++ b/tempest/tests/common/test_credentials_factory.py
@@ -183,7 +183,7 @@
# Build the expected params
expected_params = dict(
[(field, value) for _, field, value in all_params])
- expected_params.update(cf.DEFAULT_PARAMS)
+ expected_params.update(config.service_client_config())
admin_creds = cf.get_configured_admin_credentials()
mock_get_credentials.assert_called_once_with(
fill_in=True, identity_version='v3', **expected_params)
@@ -205,7 +205,7 @@
# Build the expected params
expected_params = dict(
[(field, value) for _, field, value in all_params])
- expected_params.update(cf.DEFAULT_PARAMS)
+ expected_params.update(config.service_client_config())
admin_creds = cf.get_configured_admin_credentials(
fill_in=False, identity_version='v3')
mock_get_credentials.assert_called_once_with(
@@ -232,7 +232,7 @@
cfg.CONF.set_default('uri', expected_uri, 'identity')
params = {'foo': 'bar'}
expected_params = params.copy()
- expected_params.update(cf.DEFAULT_PARAMS)
+ expected_params.update(config.service_client_config())
result = cf.get_credentials(identity_version='v2', **params)
self.assertEqual(expected_result, result)
mock_auth_get_credentials.assert_called_once_with(
@@ -251,7 +251,7 @@
params = {'foo': 'bar'}
expected_params = params.copy()
expected_params['domain_name'] = expected_domain
- expected_params.update(cf.DEFAULT_PARAMS)
+ expected_params.update(config.service_client_config())
result = cf.get_credentials(fill_in=False, identity_version='v3',
**params)
self.assertEqual(expected_result, result)
@@ -270,7 +270,7 @@
expected_domain, 'auth')
params = {'foo': 'bar', 'user_domain_name': expected_domain}
expected_params = params.copy()
- expected_params.update(cf.DEFAULT_PARAMS)
+ expected_params.update(config.service_client_config())
result = cf.get_credentials(fill_in=False, identity_version='v3',
**params)
self.assertEqual(expected_result, result)
diff --git a/tempest/tests/lib/cli/test_execute.py b/tempest/tests/lib/cli/test_execute.py
index 0130454..c276386 100644
--- a/tempest/tests/lib/cli/test_execute.py
+++ b/tempest/tests/lib/cli/test_execute.py
@@ -91,3 +91,37 @@
self.assertEqual(mock_execute.call_count, 1)
self.assertEqual(mock_execute.call_args[1],
{'prefix': 'env LAC_ALL=C'})
+
+ @mock.patch.object(cli_base, 'execute')
+ def test_execute_with_domain_name(self, mock_execute):
+ cli = cli_base.CLIClient(
+ user_domain_name='default',
+ project_domain_name='default'
+ )
+ cli.glance('action')
+ self.assertEqual(mock_execute.call_count, 1)
+ self.assertIn('--os-user-domain-name default',
+ mock_execute.call_args[0][2])
+ self.assertIn('--os-project-domain-name default',
+ mock_execute.call_args[0][2])
+ self.assertNotIn('--os-user-domain-id',
+ mock_execute.call_args[0][2])
+ self.assertNotIn('--os-project-domain-id',
+ mock_execute.call_args[0][2])
+
+ @mock.patch.object(cli_base, 'execute')
+ def test_execute_with_domain_id(self, mock_execute):
+ cli = cli_base.CLIClient(
+ user_domain_id='default',
+ project_domain_id='default'
+ )
+ cli.glance('action')
+ self.assertEqual(mock_execute.call_count, 1)
+ self.assertIn('--os-user-domain-id default',
+ mock_execute.call_args[0][2])
+ self.assertIn('--os-project-domain-id default',
+ mock_execute.call_args[0][2])
+ self.assertNotIn('--os-user-domain-name',
+ mock_execute.call_args[0][2])
+ self.assertNotIn('--os-project-domain-name',
+ mock_execute.call_args[0][2])
diff --git a/tempest/tests/lib/common/test_dynamic_creds.py b/tempest/tests/lib/common/test_dynamic_creds.py
index 6aa7a42..ebcf5d1 100644
--- a/tempest/tests/lib/common/test_dynamic_creds.py
+++ b/tempest/tests/lib/common/test_dynamic_creds.py
@@ -40,6 +40,7 @@
from tempest.tests import fake_config
from tempest.tests.lib import fake_http
from tempest.tests.lib import fake_identity
+from tempest.tests.lib.services import registry_fixture
class TestDynamicCredentialProvider(base.TestCase):
@@ -62,6 +63,7 @@
def setUp(self):
super(TestDynamicCredentialProvider, self).setUp()
self.useFixture(fake_config.ConfigFixture())
+ self.useFixture(registry_fixture.RegistryFixture())
self.patchobject(config, 'TempestConfigPrivate',
fake_config.FakePrivate)
self.patchobject(self.token_client_class, 'raw_request',
diff --git a/tempest/tests/lib/common/test_preprov_creds.py b/tempest/tests/lib/common/test_preprov_creds.py
index 5402e47..9b10159 100644
--- a/tempest/tests/lib/common/test_preprov_creds.py
+++ b/tempest/tests/lib/common/test_preprov_creds.py
@@ -32,6 +32,7 @@
from tempest.tests import base
from tempest.tests import fake_config
from tempest.tests.lib import fake_identity
+from tempest.tests.lib.services import registry_fixture
class TestPreProvisionedCredentials(base.TestCase):
@@ -92,9 +93,8 @@
return_value=self.test_accounts))
self.useFixture(fixtures.MockPatch(
'os.path.isfile', return_value=True))
- # NOTE(andreaf) Ensure config is loaded so service clients are
- # registered in the registry before tests
- config.service_client_config()
+ # Make sure we leave the registry clean
+ self.useFixture(registry_fixture.RegistryFixture())
def tearDown(self):
super(TestPreProvisionedCredentials, self).tearDown()
diff --git a/tempest/tests/lib/common/test_validation_resources.py b/tempest/tests/lib/common/test_validation_resources.py
new file mode 100644
index 0000000..d5139f4
--- /dev/null
+++ b/tempest/tests/lib/common/test_validation_resources.py
@@ -0,0 +1,344 @@
+# Copyright (c) 2017 IBM Corp.
+# 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 fixtures
+import mock
+import testtools
+
+from tempest.lib.common import validation_resources as vr
+from tempest.lib import exceptions as lib_exc
+from tempest.lib.services import clients
+from tempest.tests import base
+from tempest.tests.lib import fake_credentials
+from tempest.tests.lib.services import registry_fixture
+
+FAKE_SECURITY_GROUP = {'security_group': {'id': 'sg_id'}}
+FAKE_KEYPAIR = {'keypair': {'name': 'keypair_name'}}
+FAKE_FIP_NOVA_NET = {'floating_ip': {'ip': '1.2.3.4', 'id': '1234'}}
+FAKE_FIP_NEUTRON = {'floatingip': {'floating_ip_address': '1.2.3.4',
+ 'id': '1234'}}
+
+SERVICES = 'tempest.lib.services'
+SG_CLIENT = (SERVICES + '.%s.security_groups_client.SecurityGroupsClient.%s')
+SGR_CLIENT = (SERVICES + '.%s.security_group_rules_client.'
+ 'SecurityGroupRulesClient.create_security_group_rule')
+KP_CLIENT = (SERVICES + '.compute.keypairs_client.KeyPairsClient.%s')
+FIP_CLIENT = (SERVICES + '.%s.floating_ips_client.FloatingIPsClient.%s')
+
+
+class TestValidationResources(base.TestCase):
+
+ def setUp(self):
+ super(TestValidationResources, self).setUp()
+ self.useFixture(registry_fixture.RegistryFixture())
+ self.mock_sg_compute = self.useFixture(fixtures.MockPatch(
+ SG_CLIENT % ('compute', 'create_security_group'), autospec=True,
+ return_value=FAKE_SECURITY_GROUP))
+ self.mock_sg_network = self.useFixture(fixtures.MockPatch(
+ SG_CLIENT % ('network', 'create_security_group'), autospec=True,
+ return_value=FAKE_SECURITY_GROUP))
+ self.mock_sgr_compute = self.useFixture(fixtures.MockPatch(
+ SGR_CLIENT % 'compute', autospec=True))
+ self.mock_sgr_network = self.useFixture(fixtures.MockPatch(
+ SGR_CLIENT % 'network', autospec=True))
+ self.mock_kp = self.useFixture(fixtures.MockPatch(
+ KP_CLIENT % 'create_keypair', autospec=True,
+ return_value=FAKE_KEYPAIR))
+ self.mock_fip_compute = self.useFixture(fixtures.MockPatch(
+ FIP_CLIENT % ('compute', 'create_floating_ip'), autospec=True,
+ return_value=FAKE_FIP_NOVA_NET))
+ self.mock_fip_network = self.useFixture(fixtures.MockPatch(
+ FIP_CLIENT % ('network', 'create_floatingip'), autospec=True,
+ return_value=FAKE_FIP_NEUTRON))
+ self.os = clients.ServiceClients(
+ fake_credentials.FakeKeystoneV3Credentials(), 'fake_uri')
+
+ def test_create_ssh_security_group_nova_net(self):
+ expected_sg_id = FAKE_SECURITY_GROUP['security_group']['id']
+ sg = vr.create_ssh_security_group(self.os, add_rule=True,
+ use_neutron=False)
+ self.assertEqual(FAKE_SECURITY_GROUP['security_group'], sg)
+ # Neutron clients have not been used
+ self.assertEqual(self.mock_sg_network.mock.call_count, 0)
+ self.assertEqual(self.mock_sgr_network.mock.call_count, 0)
+ # Nova-net clients assertions
+ self.assertGreater(self.mock_sg_compute.mock.call_count, 0)
+ self.assertGreater(self.mock_sgr_compute.mock.call_count, 0)
+ for call in self.mock_sgr_compute.mock.call_args_list[1:]:
+ self.assertIn(expected_sg_id, call[1].values())
+
+ def test_create_ssh_security_group_neutron(self):
+ expected_sg_id = FAKE_SECURITY_GROUP['security_group']['id']
+ expected_ethertype = 'fake_ethertype'
+ sg = vr.create_ssh_security_group(self.os, add_rule=True,
+ use_neutron=True,
+ ethertype=expected_ethertype)
+ self.assertEqual(FAKE_SECURITY_GROUP['security_group'], sg)
+ # Nova-net clients have not been used
+ self.assertEqual(self.mock_sg_compute.mock.call_count, 0)
+ self.assertEqual(self.mock_sgr_compute.mock.call_count, 0)
+ # Nova-net clients assertions
+ self.assertGreater(self.mock_sg_network.mock.call_count, 0)
+ self.assertGreater(self.mock_sgr_network.mock.call_count, 0)
+ # Check SG ID and ethertype are passed down to rules
+ for call in self.mock_sgr_network.mock.call_args_list[1:]:
+ self.assertIn(expected_sg_id, call[1].values())
+ self.assertIn(expected_ethertype, call[1].values())
+
+ def test_create_ssh_security_no_rules(self):
+ sg = vr.create_ssh_security_group(self.os, add_rule=False)
+ self.assertEqual(FAKE_SECURITY_GROUP['security_group'], sg)
+ # SG Rules clients have not been used
+ self.assertEqual(self.mock_sgr_compute.mock.call_count, 0)
+ self.assertEqual(self.mock_sgr_network.mock.call_count, 0)
+
+ @mock.patch.object(vr, 'create_ssh_security_group',
+ return_value=FAKE_SECURITY_GROUP['security_group'])
+ def test_create_validation_resources_nova_net(self, mock_create_sg):
+ expected_floating_network_id = 'my_fni'
+ expected_floating_network_name = 'my_fnn'
+ resources = vr.create_validation_resources(
+ self.os, keypair=True, floating_ip=True, security_group=True,
+ security_group_rules=True, ethertype='IPv6', use_neutron=False,
+ floating_network_id=expected_floating_network_id,
+ floating_network_name=expected_floating_network_name)
+ # Keypair calls
+ self.assertGreater(self.mock_kp.mock.call_count, 0)
+ # Floating IP calls
+ self.assertGreater(self.mock_fip_compute.mock.call_count, 0)
+ for call in self.mock_fip_compute.mock.call_args_list[1:]:
+ self.assertIn(expected_floating_network_name, call[1].values())
+ self.assertNotIn(expected_floating_network_id, call[1].values())
+ self.assertEqual(self.mock_fip_network.mock.call_count, 0)
+ # SG calls
+ mock_create_sg.assert_called_once()
+ # Resources
+ for resource in ['keypair', 'floating_ip', 'security_group']:
+ self.assertIn(resource, resources)
+ self.assertEqual(FAKE_KEYPAIR['keypair'], resources['keypair'])
+ self.assertEqual(FAKE_SECURITY_GROUP['security_group'],
+ resources['security_group'])
+ self.assertEqual(FAKE_FIP_NOVA_NET['floating_ip'],
+ resources['floating_ip'])
+
+ @mock.patch.object(vr, 'create_ssh_security_group',
+ return_value=FAKE_SECURITY_GROUP['security_group'])
+ def test_create_validation_resources_neutron(self, mock_create_sg):
+ expected_floating_network_id = 'my_fni'
+ expected_floating_network_name = 'my_fnn'
+ resources = vr.create_validation_resources(
+ self.os, keypair=True, floating_ip=True, security_group=True,
+ security_group_rules=True, ethertype='IPv6', use_neutron=True,
+ floating_network_id=expected_floating_network_id,
+ floating_network_name=expected_floating_network_name)
+ # Keypair calls
+ self.assertGreater(self.mock_kp.mock.call_count, 0)
+ # Floating IP calls
+ self.assertEqual(self.mock_fip_compute.mock.call_count, 0)
+ self.assertGreater(self.mock_fip_network.mock.call_count, 0)
+ for call in self.mock_fip_compute.mock.call_args_list[1:]:
+ self.assertIn(expected_floating_network_id, call[1].values())
+ self.assertNotIn(expected_floating_network_name, call[1].values())
+ # SG calls
+ mock_create_sg.assert_called_once()
+ # Resources
+ for resource in ['keypair', 'floating_ip', 'security_group']:
+ self.assertIn(resource, resources)
+ self.assertEqual(FAKE_KEYPAIR['keypair'], resources['keypair'])
+ self.assertEqual(FAKE_SECURITY_GROUP['security_group'],
+ resources['security_group'])
+ self.assertIn('ip', resources['floating_ip'])
+ self.assertEqual(resources['floating_ip']['ip'],
+ FAKE_FIP_NEUTRON['floatingip']['floating_ip_address'])
+ self.assertEqual(resources['floating_ip']['id'],
+ FAKE_FIP_NEUTRON['floatingip']['id'])
+
+
+class TestClearValidationResourcesFixture(base.TestCase):
+
+ def setUp(self):
+ super(TestClearValidationResourcesFixture, self).setUp()
+ self.useFixture(registry_fixture.RegistryFixture())
+ self.mock_sg_compute = self.useFixture(fixtures.MockPatch(
+ SG_CLIENT % ('compute', 'delete_security_group'), autospec=True))
+ self.mock_sg_network = self.useFixture(fixtures.MockPatch(
+ SG_CLIENT % ('network', 'delete_security_group'), autospec=True))
+ self.mock_sg_wait_compute = self.useFixture(fixtures.MockPatch(
+ SG_CLIENT % ('compute', 'wait_for_resource_deletion'),
+ autospec=True))
+ self.mock_sg_wait_network = self.useFixture(fixtures.MockPatch(
+ SG_CLIENT % ('network', 'wait_for_resource_deletion'),
+ autospec=True))
+ self.mock_kp = self.useFixture(fixtures.MockPatch(
+ KP_CLIENT % 'delete_keypair', autospec=True))
+ self.mock_fip_compute = self.useFixture(fixtures.MockPatch(
+ FIP_CLIENT % ('compute', 'delete_floating_ip'), autospec=True))
+ self.mock_fip_network = self.useFixture(fixtures.MockPatch(
+ FIP_CLIENT % ('network', 'delete_floatingip'), autospec=True))
+ self.os = clients.ServiceClients(
+ fake_credentials.FakeKeystoneV3Credentials(), 'fake_uri')
+
+ def test_clear_validation_resources_nova_net(self):
+ vr.clear_validation_resources(
+ self.os,
+ floating_ip=FAKE_FIP_NOVA_NET['floating_ip'],
+ security_group=FAKE_SECURITY_GROUP['security_group'],
+ keypair=FAKE_KEYPAIR['keypair'],
+ use_neutron=False)
+ self.assertGreater(self.mock_kp.mock.call_count, 0)
+ for call in self.mock_kp.mock.call_args_list[1:]:
+ self.assertIn(FAKE_KEYPAIR['keypair']['name'], call[1].values())
+ self.assertGreater(self.mock_sg_compute.mock.call_count, 0)
+ for call in self.mock_sg_compute.mock.call_args_list[1:]:
+ self.assertIn(FAKE_SECURITY_GROUP['security_group']['id'],
+ call[1].values())
+ self.assertGreater(self.mock_sg_wait_compute.mock.call_count, 0)
+ for call in self.mock_sg_wait_compute.mock.call_args_list[1:]:
+ self.assertIn(FAKE_SECURITY_GROUP['security_group']['id'],
+ call[1].values())
+ self.assertEqual(self.mock_sg_network.mock.call_count, 0)
+ self.assertEqual(self.mock_sg_wait_network.mock.call_count, 0)
+ self.assertGreater(self.mock_fip_compute.mock.call_count, 0)
+ for call in self.mock_fip_compute.mock.call_args_list[1:]:
+ self.assertIn(FAKE_FIP_NOVA_NET['floating_ip']['id'],
+ call[1].values())
+ self.assertEqual(self.mock_fip_network.mock.call_count, 0)
+
+ def test_clear_validation_resources_neutron(self):
+ vr.clear_validation_resources(
+ self.os,
+ floating_ip=FAKE_FIP_NEUTRON['floatingip'],
+ security_group=FAKE_SECURITY_GROUP['security_group'],
+ keypair=FAKE_KEYPAIR['keypair'],
+ use_neutron=True)
+ self.assertGreater(self.mock_kp.mock.call_count, 0)
+ for call in self.mock_kp.mock.call_args_list[1:]:
+ self.assertIn(FAKE_KEYPAIR['keypair']['name'], call[1].values())
+ self.assertGreater(self.mock_sg_network.mock.call_count, 0)
+ for call in self.mock_sg_network.mock.call_args_list[1:]:
+ self.assertIn(FAKE_SECURITY_GROUP['security_group']['id'],
+ call[1].values())
+ self.assertGreater(self.mock_sg_wait_network.mock.call_count, 0)
+ for call in self.mock_sg_wait_network.mock.call_args_list[1:]:
+ self.assertIn(FAKE_SECURITY_GROUP['security_group']['id'],
+ call[1].values())
+ self.assertEqual(self.mock_sg_compute.mock.call_count, 0)
+ self.assertEqual(self.mock_sg_wait_compute.mock.call_count, 0)
+ self.assertGreater(self.mock_fip_network.mock.call_count, 0)
+ for call in self.mock_fip_network.mock.call_args_list[1:]:
+ self.assertIn(FAKE_FIP_NEUTRON['floatingip']['id'],
+ call[1].values())
+ self.assertEqual(self.mock_fip_compute.mock.call_count, 0)
+
+ def test_clear_validation_resources_exceptions(self):
+ # Test that even with exceptions all cleanups are invoked and that only
+ # the first exception is reported.
+ # NOTE(andreaf) There's not way of knowing which exception is going to
+ # be raised first unless we enforce which resource is cleared first,
+ # which is not really interesting, but also not harmful. keypair first.
+ self.mock_kp.mock.side_effect = Exception('keypair exception')
+ self.mock_sg_network.mock.side_effect = Exception('sg exception')
+ self.mock_fip_network.mock.side_effect = Exception('fip exception')
+ with testtools.ExpectedException(Exception, value_re='keypair'):
+ vr.clear_validation_resources(
+ self.os,
+ floating_ip=FAKE_FIP_NEUTRON['floatingip'],
+ security_group=FAKE_SECURITY_GROUP['security_group'],
+ keypair=FAKE_KEYPAIR['keypair'],
+ use_neutron=True)
+ # Clients calls are still made, but not the wait call
+ self.assertGreater(self.mock_kp.mock.call_count, 0)
+ self.assertGreater(self.mock_sg_network.mock.call_count, 0)
+ self.assertGreater(self.mock_fip_network.mock.call_count, 0)
+
+ def test_clear_validation_resources_wait_not_found_wait(self):
+ # Test that a not found on wait is not an exception
+ self.mock_sg_wait_network.mock.side_effect = lib_exc.NotFound('yay')
+ vr.clear_validation_resources(
+ self.os,
+ floating_ip=FAKE_FIP_NEUTRON['floatingip'],
+ security_group=FAKE_SECURITY_GROUP['security_group'],
+ keypair=FAKE_KEYPAIR['keypair'],
+ use_neutron=True)
+ # Clients calls are still made, but not the wait call
+ self.assertGreater(self.mock_kp.mock.call_count, 0)
+ self.assertGreater(self.mock_sg_network.mock.call_count, 0)
+ self.assertGreater(self.mock_sg_wait_network.mock.call_count, 0)
+ self.assertGreater(self.mock_fip_network.mock.call_count, 0)
+
+ def test_clear_validation_resources_wait_not_found_delete(self):
+ # Test that a not found on delete is not an exception
+ self.mock_kp.mock.side_effect = lib_exc.NotFound('yay')
+ self.mock_sg_network.mock.side_effect = lib_exc.NotFound('yay')
+ self.mock_fip_network.mock.side_effect = lib_exc.NotFound('yay')
+ vr.clear_validation_resources(
+ self.os,
+ floating_ip=FAKE_FIP_NEUTRON['floatingip'],
+ security_group=FAKE_SECURITY_GROUP['security_group'],
+ keypair=FAKE_KEYPAIR['keypair'],
+ use_neutron=True)
+ # Clients calls are still made, but not the wait call
+ self.assertGreater(self.mock_kp.mock.call_count, 0)
+ self.assertGreater(self.mock_sg_network.mock.call_count, 0)
+ self.assertEqual(self.mock_sg_wait_network.mock.call_count, 0)
+ self.assertGreater(self.mock_fip_network.mock.call_count, 0)
+
+
+class TestValidationResourcesFixture(base.TestCase):
+
+ @mock.patch.object(vr, 'create_validation_resources', autospec=True)
+ def test_use_fixture(self, mock_vr):
+ exp_vr = dict(keypair='keypair',
+ floating_ip='floating_ip',
+ security_group='security_group')
+ mock_vr.return_value = exp_vr
+ exp_clients = 'clients'
+ exp_parameters = dict(keypair=True, floating_ip=True,
+ security_group=True, security_group_rules=True,
+ ethertype='v6', use_neutron=True,
+ floating_network_id='fnid',
+ floating_network_name='fnname')
+ # First mock cleanup
+ self.useFixture(fixtures.MockPatchObject(
+ vr, 'clear_validation_resources', autospec=True))
+ # And then use vr fixture, so when the fixture is cleaned-up, the mock
+ # is still there
+ vr_fixture = self.useFixture(vr.ValidationResourcesFixture(
+ exp_clients, **exp_parameters))
+ # Assert vr have been provisioned
+ mock_vr.assert_called_once_with(exp_clients, **exp_parameters)
+ # Assert vr have been setup in the fixture
+ self.assertEqual(exp_vr, vr_fixture.resources)
+
+ @mock.patch.object(vr, 'clear_validation_resources', autospec=True)
+ @mock.patch.object(vr, 'create_validation_resources', autospec=True)
+ def test_use_fixture_context(self, mock_vr, mock_clear):
+ exp_vr = dict(keypair='keypair',
+ floating_ip='floating_ip',
+ security_group='security_group')
+ mock_vr.return_value = exp_vr
+ exp_clients = 'clients'
+ exp_parameters = dict(keypair=True, floating_ip=True,
+ security_group=True, security_group_rules=True,
+ ethertype='v6', use_neutron=True,
+ floating_network_id='fnid',
+ floating_network_name='fnname')
+ with vr.ValidationResourcesFixture(exp_clients,
+ **exp_parameters) as vr_fixture:
+ # Assert vr have been provisioned
+ mock_vr.assert_called_once_with(exp_clients, **exp_parameters)
+ # Assert vr have been setup in the fixture
+ self.assertEqual(exp_vr, vr_fixture.resources)
+ # After context manager is closed, clear is invoked
+ exp_vr['use_neutron'] = exp_parameters['use_neutron']
+ mock_clear.assert_called_once_with(exp_clients, **exp_vr)
diff --git a/tempest/tests/lib/common/utils/test_data_utils.py b/tempest/tests/lib/common/utils/test_data_utils.py
index 8bdf70e..b8385b2 100644
--- a/tempest/tests/lib/common/utils/test_data_utils.py
+++ b/tempest/tests/lib/common/utils/test_data_utils.py
@@ -13,8 +13,6 @@
# License for the specific language governing permissions and limitations
# under the License.
-import netaddr
-
from tempest.lib.common.utils import data_utils
from tempest.tests import base
@@ -81,7 +79,11 @@
self.assertEqual(len(actual), 3)
self.assertRegex(actual, "[A-Za-z0-9~!@#%^&*_=+]{3}")
actual2 = data_utils.rand_password(2)
- self.assertNotEqual(actual, actual2)
+ # NOTE(masayukig): Originally, we checked that the acutal and actual2
+ # are different each other. But only 3 letters can be the same value
+ # in a very rare case. So, we just check the length here, too,
+ # just in case.
+ self.assertEqual(len(actual2), 3)
def test_rand_url(self):
actual = data_utils.rand_url()
@@ -137,43 +139,6 @@
actual = data_utils.random_bytes(size=2048)
self.assertEqual(2048, len(actual))
- def test_get_ipv6_addr_by_EUI64(self):
- actual = data_utils.get_ipv6_addr_by_EUI64('2001:db8::',
- '00:16:3e:33:44:55')
- self.assertIsInstance(actual, netaddr.IPAddress)
- self.assertEqual(actual,
- netaddr.IPAddress('2001:db8::216:3eff:fe33:4455'))
-
- def test_get_ipv6_addr_by_EUI64_with_IPv4_prefix(self):
- ipv4_prefix = '10.0.8'
- mac = '00:16:3e:33:44:55'
- self.assertRaises(TypeError, data_utils.get_ipv6_addr_by_EUI64,
- ipv4_prefix, mac)
-
- def test_get_ipv6_addr_by_EUI64_bad_cidr_type(self):
- bad_cidr = 123
- mac = '00:16:3e:33:44:55'
- self.assertRaises(TypeError, data_utils.get_ipv6_addr_by_EUI64,
- bad_cidr, mac)
-
- def test_get_ipv6_addr_by_EUI64_bad_cidr_value(self):
- bad_cidr = 'bb'
- mac = '00:16:3e:33:44:55'
- self.assertRaises(TypeError, data_utils.get_ipv6_addr_by_EUI64,
- bad_cidr, mac)
-
- def test_get_ipv6_addr_by_EUI64_bad_mac_value(self):
- cidr = '2001:db8::'
- bad_mac = '00:16:3e:33:44:5Z'
- self.assertRaises(TypeError, data_utils.get_ipv6_addr_by_EUI64,
- cidr, bad_mac)
-
- def test_get_ipv6_addr_by_EUI64_bad_mac_type(self):
- cidr = '2001:db8::'
- bad_mac = 99999999999999999999
- self.assertRaises(TypeError, data_utils.get_ipv6_addr_by_EUI64,
- cidr, bad_mac)
-
def test_chunkify(self):
data = "aaa"
chunks = data_utils.chunkify(data, 2)
diff --git a/tempest/tests/lib/common/utils/test_test_utils.py b/tempest/tests/lib/common/utils/test_test_utils.py
index 29c5684..f638ba6 100644
--- a/tempest/tests/lib/common/utils/test_test_utils.py
+++ b/tempest/tests/lib/common/utils/test_test_utils.py
@@ -81,11 +81,13 @@
@mock.patch('time.sleep')
@mock.patch('time.time')
def test_call_until_true_when_f_never_returns_true(self, m_time, m_sleep):
+ def set_value(bool_value):
+ return bool_value
timeout = 42 # The value doesn't matter as we mock time.time()
sleep = 60 # The value doesn't matter as we mock time.sleep()
m_time.side_effect = utils.generate_timeout_series(timeout)
self.assertEqual(
- False, test_utils.call_until_true(lambda: False, timeout, sleep)
+ False, test_utils.call_until_true(set_value, timeout, sleep, False)
)
m_sleep.call_args_list = [mock.call(sleep)] * 2
m_time.call_args_list = [mock.call()] * 2
@@ -93,11 +95,30 @@
@mock.patch('time.sleep')
@mock.patch('time.time')
def test_call_until_true_when_f_returns_true(self, m_time, m_sleep):
+ def set_value(bool_value=False):
+ return bool_value
timeout = 42 # The value doesn't matter as we mock time.time()
sleep = 60 # The value doesn't matter as we mock time.sleep()
m_time.return_value = 0
self.assertEqual(
- True, test_utils.call_until_true(lambda: True, timeout, sleep)
+ True, test_utils.call_until_true(set_value, timeout, sleep,
+ bool_value=True)
)
self.assertEqual(0, m_sleep.call_count)
- self.assertEqual(1, m_time.call_count)
+ # when logging cost time we need to acquire current time.
+ self.assertEqual(2, m_time.call_count)
+
+ @mock.patch('time.sleep')
+ @mock.patch('time.time')
+ def test_call_until_true_when_f_returns_true_no_param(
+ self, m_time, m_sleep):
+ def set_value(bool_value=False):
+ return bool_value
+ timeout = 42 # The value doesn't matter as we mock time.time()
+ sleep = 60 # The value doesn't matter as we mock time.sleep()
+ m_time.side_effect = utils.generate_timeout_series(timeout)
+ self.assertEqual(
+ False, test_utils.call_until_true(set_value, timeout, sleep)
+ )
+ m_sleep.call_args_list = [mock.call(sleep)] * 2
+ m_time.call_args_list = [mock.call()] * 2
diff --git a/tempest/tests/lib/services/identity/v3/test_identity_client.py b/tempest/tests/lib/services/identity/v3/test_identity_client.py
index 6572947..3739fe6 100644
--- a/tempest/tests/lib/services/identity/v3/test_identity_client.py
+++ b/tempest/tests/lib/services/identity/v3/test_identity_client.py
@@ -60,6 +60,34 @@
}
}
+ FAKE_AUTH_DOMAINS = {
+ "domains": [
+ {
+ "description": "my domain description",
+ "enabled": True,
+ "id": "1789d1",
+ "links": {
+ "self": "https://example.com/identity/v3/domains/1789d1"
+ },
+ "name": "my domain"
+ },
+ {
+ "description": "description of my other domain",
+ "enabled": True,
+ "id": "43e8da",
+ "links": {
+ "self": "https://example.com/identity/v3/domains/43e8da"
+ },
+ "name": "another domain"
+ }
+ ],
+ "links": {
+ "self": "https://example.com/identity/v3/auth/domains",
+ "previous": None,
+ "next": None
+ }
+ }
+
def setUp(self):
super(TestIdentityClient, self).setUp()
fake_auth = fake_auth_provider.FakeAuthProvider()
@@ -89,6 +117,13 @@
self.FAKE_AUTH_PROJECTS,
bytes_body)
+ def _test_list_auth_domains(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_auth_domains,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_AUTH_DOMAINS,
+ bytes_body)
+
def test_show_api_description_with_str_body(self):
self._test_show_api_description()
@@ -122,3 +157,9 @@
def test_list_auth_projects_with_bytes_body(self):
self._test_list_auth_projects(bytes_body=True)
+
+ def test_list_auth_domains_with_str_body(self):
+ self._test_list_auth_domains()
+
+ def test_list_auth_domains_with_bytes_body(self):
+ self._test_list_auth_domains(bytes_body=True)
diff --git a/tempest/tests/lib/services/network/test_networks_client.py b/tempest/tests/lib/services/network/test_networks_client.py
new file mode 100644
index 0000000..078f4b0
--- /dev/null
+++ b/tempest/tests/lib/services/network/test_networks_client.py
@@ -0,0 +1,242 @@
+# Copyright 2017 AT&T Corporation.
+# All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import copy
+
+from tempest.lib.services.network import networks_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestNetworksClient(base.BaseServiceTest):
+
+ FAKE_NETWORKS = {
+ "networks": [
+ {
+ "admin_state_up": True,
+ "availability_zone_hints": [],
+ "availability_zones": [
+ "nova"
+ ],
+ "created_at": "2016-03-08T20:19:41",
+ "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22",
+ "mtu": 0,
+ "name": "net1",
+ "port_security_enabled": True,
+ "project_id": "4fd44f30292945e481c7b8a0c8908869",
+ "qos_policy_id": "6a8454ade84346f59e8d40665f878b2e",
+ "router:external": False,
+ "shared": False,
+ "status": "ACTIVE",
+ "subnets": [
+ "54d6f61d-db07-451c-9ab3-b9609b6b6f0b"
+ ],
+ "tenant_id": "4fd44f30292945e481c7b8a0c8908869",
+ "updated_at": "2016-03-08T20:19:41",
+ "vlan_transparent": True,
+ "description": ""
+ },
+ {
+ "admin_state_up": True,
+ "availability_zone_hints": [],
+ "availability_zones": [
+ "nova"
+ ],
+ "id": "db193ab3-96e3-4cb3-8fc5-05f4296d0324",
+ "mtu": 0,
+ "name": "net2",
+ "port_security_enabled": True,
+ "project_id": "26a7980765d0414dbc1fc1f88cdb7e6e",
+ "qos_policy_id": "bfdb6c39f71e4d44b1dfbda245c50819",
+ "router:external": False,
+ "shared": False,
+ "status": "ACTIVE",
+ "subnets": [
+ "08eae331-0402-425a-923c-34f7cfe39c1b"
+ ],
+ "tenant_id": "26a7980765d0414dbc1fc1f88cdb7e6e",
+ "updated_at": "2016-03-08T20:19:41",
+ "vlan_transparent": False,
+ "description": ""
+ }
+ ]
+ }
+
+ FAKE_NETWORK_ID = "d32019d3-bc6e-4319-9c1d-6722fc136a22"
+
+ FAKE_NETWORK1 = {
+ "name": "net1",
+ "admin_state_up": True,
+ "qos_policy_id": "6a8454ade84346f59e8d40665f878b2e"
+ }
+
+ FAKE_NETWORK2 = {
+ "name": "net2",
+ "admin_state_up": True,
+ "qos_policy_id": "bfdb6c39f71e4d44b1dfbda245c50819"
+ }
+
+ FAKE_NETWORKS_REQ = {
+ "networks": [
+ FAKE_NETWORK1,
+ FAKE_NETWORK2
+ ]
+ }
+
+ FAKE_DHCP_AGENT_NETWORK_ID = "80515c45-651f-4f9a-b82b-2ca8a7301a8d"
+
+ FAKE_DHCP_AGENTS = {
+ "agents": [
+ {
+ "binary": "neutron-dhcp-agent",
+ "description": None,
+ "admin_state_up": True,
+ "heartbeat_timestamp": "2017-06-22 18:29:50",
+ "availability_zone": "nova",
+ "alive": True,
+ "topic": "dhcp_agent",
+ "host": "osboxes",
+ "agent_type": "DHCP agent",
+ "resource_versions": {},
+ "created_at": "2017-06-19 21:39:51",
+ "started_at": "2017-06-19 21:39:51",
+ "id": "b6cfb7a1-6ac4-4980-993c-9d295d37062e",
+ "configurations": {
+ "subnets": 2,
+ "dhcp_lease_duration": 86400,
+ "dhcp_driver": "neutron.agent.linux.dhcp.Dnsmasq",
+ "networks": 1,
+ "log_agent_heartbeats": False,
+ "ports": 3
+ }
+ }
+ ]
+ }
+
+ def setUp(self):
+ super(TestNetworksClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.networks_client = networks_client.NetworksClient(
+ fake_auth, "network", "regionOne")
+
+ def _test_list_networks(self, bytes_body=False):
+ self.check_service_client_function(
+ self.networks_client.list_networks,
+ "tempest.lib.common.rest_client.RestClient.get",
+ self.FAKE_NETWORKS,
+ bytes_body,
+ 200)
+
+ def _test_create_network(self, bytes_body=False):
+ self.check_service_client_function(
+ self.networks_client.create_network,
+ "tempest.lib.common.rest_client.RestClient.post",
+ {"network": self.FAKE_NETWORKS["networks"][0]},
+ bytes_body,
+ 201,
+ **self.FAKE_NETWORK1)
+
+ def _test_create_bulk_networks(self, bytes_body=False):
+ self.check_service_client_function(
+ self.networks_client.create_bulk_networks,
+ "tempest.lib.common.rest_client.RestClient.post",
+ self.FAKE_NETWORKS,
+ bytes_body,
+ 201,
+ networks=self.FAKE_NETWORKS_REQ)
+
+ def _test_show_network(self, bytes_body=False):
+ self.check_service_client_function(
+ self.networks_client.show_network,
+ "tempest.lib.common.rest_client.RestClient.get",
+ {"network": self.FAKE_NETWORKS["networks"][0]},
+ bytes_body,
+ 200,
+ network_id=self.FAKE_NETWORK_ID)
+
+ def _test_update_network(self, bytes_body=False):
+ update_kwargs = {
+ "name": "sample_network_5_updated",
+ "qos_policy_id": "6a8454ade84346f59e8d40665f878b2e"
+ }
+
+ resp_body = {
+ "network": copy.deepcopy(
+ self.FAKE_NETWORKS["networks"][0]
+ )
+ }
+ resp_body["network"].update(update_kwargs)
+
+ self.check_service_client_function(
+ self.networks_client.update_network,
+ "tempest.lib.common.rest_client.RestClient.put",
+ resp_body,
+ bytes_body,
+ 200,
+ network_id=self.FAKE_NETWORK_ID,
+ **update_kwargs)
+
+ def _test_list_dhcp_agents_on_hosting_network(self, bytes_body=False):
+ self.check_service_client_function(
+ self.networks_client.list_dhcp_agents_on_hosting_network,
+ "tempest.lib.common.rest_client.RestClient.get",
+ self.FAKE_DHCP_AGENTS,
+ bytes_body,
+ 200,
+ network_id=self.FAKE_DHCP_AGENT_NETWORK_ID)
+
+ def test_delete_network(self):
+ self.check_service_client_function(
+ self.networks_client.delete_network,
+ "tempest.lib.common.rest_client.RestClient.delete",
+ {},
+ status=204,
+ network_id=self.FAKE_NETWORK_ID)
+
+ def test_list_networks_with_str_body(self):
+ self._test_list_networks()
+
+ def test_list_networks_with_bytes_body(self):
+ self._test_list_networks(bytes_body=True)
+
+ def test_create_network_with_str_body(self):
+ self._test_create_network()
+
+ def test_create_network_with_bytes_body(self):
+ self._test_create_network(bytes_body=True)
+
+ def test_create_bulk_network_with_str_body(self):
+ self._test_create_bulk_networks()
+
+ def test_create_bulk_network_with_bytes_body(self):
+ self._test_create_bulk_networks(bytes_body=True)
+
+ def test_show_network_with_str_body(self):
+ self._test_show_network()
+
+ def test_show_network_with_bytes_body(self):
+ self._test_show_network(bytes_body=True)
+
+ def test_update_network_with_str_body(self):
+ self._test_update_network()
+
+ def test_update_network_with_bytes_body(self):
+ self._test_update_network(bytes_body=True)
+
+ def test_list_dhcp_agents_on_hosting_network_with_str_body(self):
+ self._test_list_dhcp_agents_on_hosting_network()
+
+ def test_list_dhcp_agents_on_hosting_network_with_bytes_body(self):
+ self._test_list_dhcp_agents_on_hosting_network(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
new file mode 100644
index 0000000..a16d1d7
--- /dev/null
+++ b/tempest/tests/lib/services/object_storage/test_object_client.py
@@ -0,0 +1,108 @@
+# Copyright 2016 IBM Corp.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+
+import mock
+
+from tempest.lib import exceptions
+from tempest.lib.services.object_storage import object_client
+from tempest.tests import base
+from tempest.tests.lib import fake_auth_provider
+
+
+class TestObjectClient(base.TestCase):
+
+ def setUp(self):
+ super(TestObjectClient, self).setUp()
+ self.fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.url = self.fake_auth.base_url(None)
+ self.object_client = object_client.ObjectClient(self.fake_auth,
+ 'swift', 'region1')
+
+ @mock.patch.object(object_client, '_create_connection')
+ def test_create_object_continue_no_data(self, mock_poc):
+ self._validate_create_object_continue(None, mock_poc)
+
+ @mock.patch.object(object_client, '_create_connection')
+ def test_create_object_continue_with_data(self, mock_poc):
+ self._validate_create_object_continue('hello', mock_poc)
+
+ @mock.patch.object(object_client, '_create_connection')
+ def test_create_continue_with_no_continue_received(self, mock_poc):
+ self._validate_create_object_continue('hello', mock_poc,
+ initial_status=201)
+
+ def _validate_create_object_continue(self, req_data,
+ mock_poc, initial_status=100):
+
+ expected_hdrs = {
+ 'X-Auth-Token': self.fake_auth.get_token(),
+ 'content-length': 0 if req_data is None else len(req_data),
+ 'Expect': '100-continue'}
+
+ # Setup the Mocks prior to invoking the object creation
+ mock_resp_cls = mock.Mock()
+ mock_resp_cls._read_status.return_value = ("1", initial_status, "OK")
+
+ mock_poc.return_value.response_class.return_value = mock_resp_cls
+
+ # This is the final expected return value
+ mock_poc.return_value.getresponse.return_value.status = 201
+ mock_poc.return_value.getresponse.return_value.reason = 'OK'
+
+ # Call method to PUT object using expect:100-continue
+ cnt = "container1"
+ obj = "object1"
+ path = "/%s/%s" % (cnt, obj)
+
+ # If the expected initial status is not 100, then an exception
+ # should be thrown and the connection closed
+ if initial_status is 100:
+ status, reason = \
+ self.object_client.create_object_continue(cnt, obj, req_data)
+ else:
+ self.assertRaises(exceptions.UnexpectedResponseCode,
+ self.object_client.create_object_continue, cnt,
+ obj, req_data)
+ mock_poc.return_value.close.assert_called_once_with()
+
+ # Verify that putrequest is called 1 time with the appropriate values
+ mock_poc.return_value.putrequest.assert_called_once_with('PUT', path)
+
+ # Verify that headers were written, including "Expect:100-continue"
+ calls = []
+
+ for header, value in expected_hdrs.items():
+ calls.append(mock.call(header, value))
+
+ mock_poc.return_value.putheader.assert_has_calls(calls, False)
+ 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:
+ # Verify that the method returned what it was supposed to
+ self.assertEqual(status, 201)
+
+ # Verify that _safe_read was called once to remove the CRLF
+ # after the 100 response
+ mock_rc = mock_poc.return_value.response_class.return_value
+ mock_rc._safe_read.assert_called_once_with(2)
+
+ # Verify the actual data was written via send
+ mock_poc.return_value.send.assert_called_once_with(req_data)
+
+ # Verify that the getresponse method was called to receive
+ # the final
+ mock_poc.return_value.getresponse.assert_called_once_with()
diff --git a/tempest/tests/lib/services/registry_fixture.py b/tempest/tests/lib/services/registry_fixture.py
new file mode 100644
index 0000000..1da2112
--- /dev/null
+++ b/tempest/tests/lib/services/registry_fixture.py
@@ -0,0 +1,65 @@
+# Copyright 2017 IBM Corp.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import fixtures
+
+from tempest.lib.services import clients
+
+
+class RegistryFixture(fixtures.Fixture):
+ """A fixture to setup a test client registry
+
+ The clients registry is a singleton. In Tempest it's filled with
+ content from configuration. When testing Tempest lib classes without
+ configuration it's handy to have the registry setup to be able to access
+ service client factories.
+
+ This fixture sets up the registry using a fake plugin, which includes all
+ services specified at __init__ time. Any other plugin in the registry
+ is removed at setUp time. The fake plugin is removed from the registry
+ on cleanup.
+ """
+
+ PLUGIN_NAME = 'fake_plugin_for_test'
+
+ def __init__(self):
+ """Initialise the registry fixture"""
+ self.services = set(['compute', 'identity.v2', 'identity.v3',
+ 'image.v1', 'image.v2', 'network', 'volume.v1',
+ 'volume.v2', 'volume.v3', 'object-storage'])
+
+ def _setUp(self):
+ # Cleanup the registry
+ registry = clients.ClientsRegistry()
+ registry._service_clients = {}
+ # Prepare the clients for registration
+ all_clients = []
+ service_clients = clients.tempest_modules()
+ for sc in self.services:
+ sc_module = service_clients[sc]
+ sc_unversioned = sc.split('.')[0]
+ sc_name = sc.replace('.', '_').replace('-', '_')
+ # Pass the bare minimum params to satisfy the clients interface
+ service_client_data = dict(
+ name=sc_name, service_version=sc, service=sc_unversioned,
+ module_path=sc_module.__name__,
+ client_names=sc_module.__all__)
+ all_clients.append(service_client_data)
+ registry.register_service_client(self.PLUGIN_NAME, all_clients)
+
+ def _cleanup():
+ del registry._service_clients[self.PLUGIN_NAME]
+
+ self.addCleanup(_cleanup)
diff --git a/tempest/tests/lib/services/test_clients.py b/tempest/tests/lib/services/test_clients.py
index 6d0f27a..43fd88f 100644
--- a/tempest/tests/lib/services/test_clients.py
+++ b/tempest/tests/lib/services/test_clients.py
@@ -189,9 +189,7 @@
def setUp(self):
super(TestServiceClients, self).setUp()
self.useFixture(fixtures.MockPatch(
- 'tempest.lib.services.clients.tempest_modules', return_value={}))
- self.useFixture(fixtures.MockPatch(
- 'tempest.lib.services.clients._tempest_internal_modules',
+ 'tempest.lib.services.clients.tempest_modules',
return_value=set(['fake_service1'])))
def test___init___creds_v2_uri(self):
@@ -416,6 +414,7 @@
_manager = self._get_manager()
duplicate_service = 'fake_service1'
expected_error = '.*' + duplicate_service
+ _manager._registered_services = [duplicate_service]
with testtools.ExpectedException(
exceptions.ServiceClientRegistrationException, expected_error):
_manager.register_service_client_module(
diff --git a/tempest/tests/lib/services/volume/v3/test_group_snapshots_client.py b/tempest/tests/lib/services/volume/v3/test_group_snapshots_client.py
index 5ac5c08..c2784b2 100644
--- a/tempest/tests/lib/services/volume/v3/test_group_snapshots_client.py
+++ b/tempest/tests/lib/services/volume/v3/test_group_snapshots_client.py
@@ -93,7 +93,8 @@
bytes_body,
group_snapshot_id="3fbbcccf-d058-4502-8844-6feeffdf4cb5")
- def _test_list_group_snapshots(self, bytes_body=False, detail=False):
+ def _test_list_group_snapshots(self, detail=False, bytes_body=False,
+ mock_args='group_snapshots', **params):
resp_body = []
if detail:
resp_body = self.FAKE_LIST_GROUP_SNAPSHOTS
@@ -111,8 +112,10 @@
self.client.list_group_snapshots,
'tempest.lib.common.rest_client.RestClient.get',
resp_body,
- bytes_body,
- detail=detail)
+ to_utf=bytes_body,
+ mock_args=[mock_args],
+ detail=detail,
+ **params)
def test_create_group_snapshot_with_str_body(self):
self._test_create_group_snapshot()
@@ -132,6 +135,25 @@
def test_list_group_snapshots_with_bytes_body(self):
self._test_list_group_snapshots(bytes_body=True)
+ def test_list_group_snapshots_with_detail_with_str_body(self):
+ mock_args = "group_snapshots/detail"
+ self._test_list_group_snapshots(detail=True, mock_args=mock_args)
+
+ def test_list_group_snapshots_with_detail_with_bytes_body(self):
+ mock_args = "group_snapshots/detail"
+ self._test_list_group_snapshots(detail=True, bytes_body=True,
+ mock_args=mock_args)
+
+ def test_list_group_snapshots_with_params(self):
+ # Run the test separately for each param, to avoid assertion error
+ # resulting from randomized params order.
+ mock_args = 'group_snapshots?sort_key=name'
+ self._test_list_group_snapshots(mock_args=mock_args, sort_key='name')
+
+ mock_args = 'group_snapshots/detail?limit=10'
+ self._test_list_group_snapshots(detail=True, bytes_body=True,
+ mock_args=mock_args, limit=10)
+
def test_delete_group_snapshot(self):
self.check_service_client_function(
self.client.delete_group_snapshot,
@@ -139,3 +161,12 @@
{},
group_snapshot_id='0e701ab8-1bec-4b9f-b026-a7ba4af13578',
status=202)
+
+ def test_reset_group_snapshot_status(self):
+ self.check_service_client_function(
+ self.client.reset_group_snapshot_status,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ {},
+ status=202,
+ group_snapshot_id='0e701ab8-1bec-4b9f-b026-a7ba4af13578',
+ status_to_set='error')
diff --git a/tempest/tests/lib/services/volume/v3/test_groups_client.py b/tempest/tests/lib/services/volume/v3/test_groups_client.py
index 0884e5a..918e958 100644
--- a/tempest/tests/lib/services/volume/v3/test_groups_client.py
+++ b/tempest/tests/lib/services/volume/v3/test_groups_client.py
@@ -184,3 +184,12 @@
group_id='0e701ab8-1bec-4b9f-b026-a7ba4af13578',
status=202,
**self.FAKE_UPDATE_GROUP['group'])
+
+ def test_reset_group_status(self):
+ self.check_service_client_function(
+ self.client.reset_group_status,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ {},
+ status=202,
+ group_id='0e701ab8-1bec-4b9f-b026-a7ba4af13578',
+ status_to_set='error')
diff --git a/tempest/tests/lib/test_decorators.py b/tempest/tests/lib/test_decorators.py
index bbebcd3..ed0eea3 100644
--- a/tempest/tests/lib/test_decorators.py
+++ b/tempest/tests/lib/test_decorators.py
@@ -125,35 +125,6 @@
self.assertRaises(ValueError, self._test_helper, _id)
-class TestSkipUnlessAttrDecorator(base.TestCase):
- def _test_skip_unless_attr(self, attr, expected_to_skip=True):
- class TestFoo(test.BaseTestCase):
- expected_attr = not expected_to_skip
-
- @decorators.skip_unless_attr(attr)
- def test_foo(self):
- pass
-
- t = TestFoo('test_foo')
- if expected_to_skip:
- self.assertRaises(testtools.TestCase.skipException,
- t.test_foo)
- else:
- try:
- t.test_foo()
- except Exception:
- raise testtools.TestCase.failureException()
-
- def test_skip_attr_does_not_exist(self):
- self._test_skip_unless_attr('unexpected_attr')
-
- def test_skip_attr_false(self):
- self._test_skip_unless_attr('expected_attr')
-
- def test_no_skip_for_attr_exist_and_true(self):
- self._test_skip_unless_attr('expected_attr', expected_to_skip=False)
-
-
class TestRelatedBugDecorator(base.TestCase):
def test_relatedbug_when_no_exception(self):
f = mock.Mock()
diff --git a/tempest/tests/services/__init__.py b/tempest/tests/services/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/tempest/tests/services/__init__.py
+++ /dev/null
diff --git a/tempest/tests/services/object_storage/__init__.py b/tempest/tests/services/object_storage/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/tempest/tests/services/object_storage/__init__.py
+++ /dev/null
diff --git a/tempest/tests/services/object_storage/test_object_client.py b/tempest/tests/services/object_storage/test_object_client.py
index 748614c..86535f9 100644
--- a/tempest/tests/services/object_storage/test_object_client.py
+++ b/tempest/tests/services/object_storage/test_object_client.py
@@ -31,15 +31,15 @@
self.object_client = object_client.ObjectClient(self.fake_auth,
'swift', 'region1')
- @mock.patch.object(object_client, 'create_connection')
+ @mock.patch.object(object_client, '_create_connection')
def test_create_object_continue_no_data(self, mock_poc):
self._validate_create_object_continue(None, mock_poc)
- @mock.patch.object(object_client, 'create_connection')
+ @mock.patch.object(object_client, '_create_connection')
def test_create_object_continue_with_data(self, mock_poc):
self._validate_create_object_continue('hello', mock_poc)
- @mock.patch.object(object_client, 'create_connection')
+ @mock.patch.object(object_client, '_create_connection')
def test_create_continue_with_no_continue_received(self, mock_poc):
self._validate_create_object_continue('hello', mock_poc,
initial_status=201)
diff --git a/tempest/tests/test_base_test.py b/tempest/tests/test_base_test.py
index 3ece11d..011bc9b 100644
--- a/tempest/tests/test_base_test.py
+++ b/tempest/tests/test_base_test.py
@@ -17,6 +17,7 @@
from tempest import clients
from tempest.common import credentials_factory as credentials
+from tempest import config
from tempest.lib.common import fixed_network
from tempest import test
from tempest.tests import base
@@ -27,6 +28,8 @@
def setUp(self):
super(TestBaseTestCase, self).setUp()
self.useFixture(fake_config.ConfigFixture())
+ self.patchobject(config, 'TempestConfigPrivate',
+ fake_config.FakePrivate)
self.fixed_network_name = 'fixed-net'
cfg.CONF.set_default('fixed_network_name', self.fixed_network_name,
'compute')
diff --git a/tempest/tests/test_decorators.py b/tempest/tests/test_decorators.py
index bf04280..6018441 100644
--- a/tempest/tests/test_decorators.py
+++ b/tempest/tests/test_decorators.py
@@ -176,96 +176,3 @@
self._test_requires_ext_helper,
extension='enabled_ext',
service='bad_service')
-
-
-class TestConfigDecorators(BaseDecoratorsTest):
- def setUp(self):
- super(TestConfigDecorators, self).setUp()
- cfg.CONF.set_default('nova', True, 'service_available')
- cfg.CONF.set_default('glance', False, 'service_available')
-
- def _assert_skip_message(self, func, skip_msg):
- try:
- func()
- self.fail()
- except testtools.TestCase.skipException as skip_exc:
- self.assertEqual(skip_exc.args[0], skip_msg)
-
- def _test_skip_unless_config(self, expected_to_skip=True, *decorator_args):
-
- class TestFoo(test.BaseTestCase):
- @config.skip_unless_config(*decorator_args)
- def test_bar(self):
- return 0
-
- t = TestFoo('test_bar')
- if expected_to_skip:
- self.assertRaises(testtools.TestCase.skipException, t.test_bar)
- if (len(decorator_args) >= 3):
- # decorator_args[2]: skip message specified
- self._assert_skip_message(t.test_bar, decorator_args[2])
- else:
- try:
- self.assertEqual(t.test_bar(), 0)
- except testtools.TestCase.skipException:
- # We caught a skipException but we didn't expect to skip
- # this test so raise a hard test failure instead.
- raise testtools.TestCase.failureException(
- "Not supposed to skip")
-
- def _test_skip_if_config(self, expected_to_skip=True,
- *decorator_args):
-
- class TestFoo(test.BaseTestCase):
- @config.skip_if_config(*decorator_args)
- def test_bar(self):
- return 0
-
- t = TestFoo('test_bar')
- if expected_to_skip:
- self.assertRaises(testtools.TestCase.skipException, t.test_bar)
- if (len(decorator_args) >= 3):
- # decorator_args[2]: skip message specified
- self._assert_skip_message(t.test_bar, decorator_args[2])
- else:
- try:
- self.assertEqual(t.test_bar(), 0)
- except testtools.TestCase.skipException:
- # We caught a skipException but we didn't expect to skip
- # this test so raise a hard test failure instead.
- raise testtools.TestCase.failureException(
- "Not supposed to skip")
-
- def test_skip_unless_no_group(self):
- self._test_skip_unless_config(True, 'fake_group', 'an_option')
-
- def test_skip_unless_no_option(self):
- self._test_skip_unless_config(True, 'service_available',
- 'not_an_option')
-
- def test_skip_unless_false_option(self):
- self._test_skip_unless_config(True, 'service_available', 'glance')
-
- def test_skip_unless_false_option_msg(self):
- self._test_skip_unless_config(True, 'service_available', 'glance',
- 'skip message')
-
- def test_skip_unless_true_option(self):
- self._test_skip_unless_config(False,
- 'service_available', 'nova')
-
- def test_skip_if_no_group(self):
- self._test_skip_if_config(False, 'fake_group', 'an_option')
-
- def test_skip_if_no_option(self):
- self._test_skip_if_config(False, 'service_available', 'not_an_option')
-
- def test_skip_if_false_option(self):
- self._test_skip_if_config(False, 'service_available', 'glance')
-
- def test_skip_if_true_option(self):
- self._test_skip_if_config(True, 'service_available', 'nova')
-
- def test_skip_if_true_option_msg(self):
- self._test_skip_if_config(True, 'service_available', 'nova',
- 'skip message')
diff --git a/tempest/tests/test_imports.py b/tempest/tests/test_imports.py
new file mode 100644
index 0000000..6f1cfca
--- /dev/null
+++ b/tempest/tests/test_imports.py
@@ -0,0 +1,69 @@
+# Copyright 2017 IBM Corp.
+#
+# 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 mock
+
+from tempest.tests import base
+
+
+class ConfCounter(object):
+
+ def __init__(self, *args, **kwargs):
+ self.count = 0
+
+ def __getattr__(self, key):
+ self.count += 1
+ return mock.MagicMock()
+
+ def get_counts(self):
+ return self.count
+
+
+class TestImports(base.TestCase):
+ def setUp(self):
+ super(TestImports, self).setUp()
+ self.conf_mock = self.patch('tempest.config.CONF',
+ new_callable=ConfCounter)
+
+ def test_account_generator_command_import(self):
+ from tempest.cmd import account_generator # noqa
+ self.assertEqual(0, self.conf_mock.get_counts())
+
+ def test_cleanup_command_import(self):
+ from tempest.cmd import cleanup # noqa
+ self.assertEqual(0, self.conf_mock.get_counts())
+
+ def test_init_command_import(self):
+ from tempest.cmd import init # noqa
+ self.assertEqual(0, self.conf_mock.get_counts())
+
+ def test_list_plugins_command_import(self):
+ from tempest.cmd import list_plugins # noqa
+ self.assertEqual(0, self.conf_mock.get_counts())
+
+ def test_run_command_import(self):
+ from tempest.cmd import run # noqa
+ self.assertEqual(0, self.conf_mock.get_counts())
+
+ def test_subunit_descibe_command_import(self):
+ from tempest.cmd import subunit_describe_calls # noqa
+ self.assertEqual(0, self.conf_mock.get_counts())
+
+ def test_verify_tempest_config_command_import(self):
+ from tempest.cmd import verify_tempest_config # noqa
+ self.assertEqual(0, self.conf_mock.get_counts())
+
+ def test_workspace_command_import(self):
+ from tempest.cmd import workspace # noqa
+ self.assertEqual(0, self.conf_mock.get_counts())
diff --git a/tempest/tests/test_list_tests.py b/tempest/tests/test_list_tests.py
index a238879..4af7463 100644
--- a/tempest/tests/test_list_tests.py
+++ b/tempest/tests/test_list_tests.py
@@ -23,12 +23,10 @@
class TestTestList(base.TestCase):
- def test_testr_list_tests_no_errors(self):
- # Remove unit test discover path from env to test tempest tests
+ def test_stestr_list_no_errors(self):
test_env = os.environ.copy()
- test_env.pop('OS_TEST_PATH')
import_failures = []
- p = subprocess.Popen(['testr', 'list-tests'], stdout=subprocess.PIPE,
+ p = subprocess.Popen(['stestr', 'list'], stdout=subprocess.PIPE,
env=test_env)
ids, err = p.communicate()
self.assertEqual(0, p.returncode,
diff --git a/tempest/tests/test_tempest_plugin.py b/tempest/tests/test_tempest_plugin.py
index 13e2499..ddadef5 100644
--- a/tempest/tests/test_tempest_plugin.py
+++ b/tempest/tests/test_tempest_plugin.py
@@ -17,9 +17,16 @@
from tempest.test_discover import plugins
from tempest.tests import base
from tempest.tests import fake_tempest_plugin as fake_plugin
+from tempest.tests.lib.services import registry_fixture
class TestPluginDiscovery(base.TestCase):
+
+ def setUp(self):
+ super(TestPluginDiscovery, self).setUp()
+ # Make sure we leave the registry clean
+ self.useFixture(registry_fixture.RegistryFixture())
+
def test_load_tests_with_one_plugin(self):
# we can't mock stevedore since it's a singleton and already executed
# during test discovery. So basically this test covers the plugin loop
diff --git a/tempest/tests/test_test.py b/tempest/tests/test_test.py
new file mode 100644
index 0000000..fc50736
--- /dev/null
+++ b/tempest/tests/test_test.py
@@ -0,0 +1,626 @@
+# Copyright 2017 IBM Corp
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import os
+import sys
+
+import mock
+from oslo_config import cfg
+import testtools
+
+from tempest import clients
+from tempest import config
+from tempest.lib.common import validation_resources as vr
+from tempest.lib import exceptions as lib_exc
+from tempest import test
+from tempest.tests import base
+from tempest.tests import fake_config
+from tempest.tests.lib import fake_credentials
+from tempest.tests.lib.services import registry_fixture
+
+
+if sys.version_info >= (2, 7):
+ import unittest
+else:
+ import unittest2 as unittest
+
+
+class LoggingTestResult(testtools.TestResult):
+
+ def __init__(self, log, *args, **kwargs):
+ super(LoggingTestResult, self).__init__(*args, **kwargs)
+ self.log = log
+
+ def addError(self, test, err=None, details=None):
+ self.log.append((test, err, details))
+
+
+class TestValidationResources(base.TestCase):
+
+ validation_resources_module = 'tempest.lib.common.validation_resources'
+
+ def setUp(self):
+ super(TestValidationResources, self).setUp()
+ self.useFixture(fake_config.ConfigFixture())
+ self.useFixture(registry_fixture.RegistryFixture())
+ self.patchobject(config, 'TempestConfigPrivate',
+ fake_config.FakePrivate)
+
+ class TestTestClass(test.BaseTestCase):
+ pass
+
+ self.test_test_class = TestTestClass
+
+ def test_validation_resources_no_validation(self):
+ cfg.CONF.set_default('run_validation', False, 'validation')
+ creds = fake_credentials.FakeKeystoneV3Credentials()
+ osclients = clients.Manager(creds)
+ vr = self.test_test_class.get_class_validation_resources(osclients)
+ self.assertIsNone(vr)
+
+ def test_validation_resources_exists(self):
+ cfg.CONF.set_default('run_validation', True, 'validation')
+ creds = fake_credentials.FakeKeystoneV3Credentials()
+ osclients = clients.Manager(creds)
+ expected_vr = 'expected_validation_resources'
+ self.test_test_class._validation_resources[osclients] = expected_vr
+ obtained_vr = self.test_test_class.get_class_validation_resources(
+ osclients)
+ self.assertEqual(expected_vr, obtained_vr)
+
+ @mock.patch(validation_resources_module + '.create_validation_resources',
+ autospec=True)
+ def test_validation_resources_new(self, mock_create_vr):
+ cfg.CONF.set_default('run_validation', True, 'validation')
+ cfg.CONF.set_default('neutron', True, 'service_available')
+ creds = fake_credentials.FakeKeystoneV3Credentials()
+ osclients = clients.Manager(creds)
+ expected_vr = {'expected_validation_resources': None}
+ mock_create_vr.return_value = expected_vr
+ with mock.patch.object(
+ self.test_test_class,
+ 'addClassResourceCleanup') as mock_add_class_cleanup:
+ obtained_vr = self.test_test_class.get_class_validation_resources(
+ osclients)
+ self.assertEqual(1, mock_add_class_cleanup.call_count)
+ self.assertEqual(mock.call(vr.clear_validation_resources,
+ osclients,
+ use_neutron=True,
+ **expected_vr),
+ mock_add_class_cleanup.call_args)
+ self.assertEqual(mock_create_vr.call_count, 1)
+ self.assertIn(osclients, mock_create_vr.call_args_list[0][0])
+ self.assertEqual(expected_vr, obtained_vr)
+ self.assertIn(osclients, self.test_test_class._validation_resources)
+ self.assertEqual(expected_vr,
+ self.test_test_class._validation_resources[osclients])
+
+ def test_validation_resources_invalid_config(self):
+ invalid_version = 999
+ cfg.CONF.set_default('run_validation', True, 'validation')
+ cfg.CONF.set_default('ip_version_for_ssh', invalid_version,
+ 'validation')
+ cfg.CONF.set_default('neutron', True, 'service_available')
+ creds = fake_credentials.FakeKeystoneV3Credentials()
+ osclients = clients.Manager(creds)
+ with testtools.ExpectedException(
+ lib_exc.InvalidConfiguration,
+ value_re='^.*\n.*' + str(invalid_version)):
+ self.test_test_class.get_class_validation_resources(osclients)
+
+ @mock.patch(validation_resources_module + '.create_validation_resources',
+ autospec=True)
+ def test_validation_resources_invalid_config_nova_net(self,
+ mock_create_vr):
+ invalid_version = 999
+ cfg.CONF.set_default('run_validation', True, 'validation')
+ cfg.CONF.set_default('ip_version_for_ssh', invalid_version,
+ 'validation')
+ cfg.CONF.set_default('neutron', False, 'service_available')
+ creds = fake_credentials.FakeKeystoneV3Credentials()
+ osclients = clients.Manager(creds)
+ expected_vr = {'expected_validation_resources': None}
+ mock_create_vr.return_value = expected_vr
+ obtained_vr = self.test_test_class.get_class_validation_resources(
+ osclients)
+ self.assertEqual(mock_create_vr.call_count, 1)
+ self.assertIn(osclients, mock_create_vr.call_args_list[0][0])
+ self.assertEqual(expected_vr, obtained_vr)
+ self.assertIn(osclients, self.test_test_class._validation_resources)
+ self.assertEqual(expected_vr,
+ self.test_test_class._validation_resources[osclients])
+
+ @mock.patch(validation_resources_module + '.create_validation_resources',
+ autospec=True)
+ @mock.patch(validation_resources_module + '.clear_validation_resources',
+ autospec=True)
+ def test_validation_resources_fixture(self, mock_clean_vr, mock_create_vr):
+
+ class TestWithRun(self.test_test_class):
+
+ def runTest(self):
+ pass
+
+ cfg.CONF.set_default('run_validation', True, 'validation')
+ test_case = TestWithRun()
+ creds = fake_credentials.FakeKeystoneV3Credentials()
+ osclients = clients.Manager(creds)
+ test_case.get_test_validation_resources(osclients)
+ self.assertEqual(1, mock_create_vr.call_count)
+ self.assertEqual(0, mock_clean_vr.call_count)
+
+
+class TestSetNetworkResources(base.TestCase):
+
+ def setUp(self):
+ super(TestSetNetworkResources, self).setUp()
+
+ class ParentTest(test.BaseTestCase):
+
+ @classmethod
+ def setup_credentials(cls):
+ cls.set_network_resources(dhcp=True)
+ super(ParentTest, cls).setup_credentials()
+
+ def runTest(self):
+ pass
+
+ self.parent_class = ParentTest
+
+ def test_set_network_resources_child_only(self):
+
+ class ChildTest(self.parent_class):
+
+ @classmethod
+ def setup_credentials(cls):
+ cls.set_network_resources(router=True)
+ super(ChildTest, cls).setup_credentials()
+
+ child_test = ChildTest()
+ child_test.setUpClass()
+ # Assert that the parents network resources are not set
+ self.assertFalse(child_test._network_resources['dhcp'])
+ # Assert that the child network resources are set
+ self.assertTrue(child_test._network_resources['router'])
+
+ def test_set_network_resources_right_order(self):
+
+ class ChildTest(self.parent_class):
+
+ @classmethod
+ def setup_credentials(cls):
+ super(ChildTest, cls).setup_credentials()
+ cls.set_network_resources(router=True)
+
+ child_test = ChildTest()
+ with testtools.ExpectedException(RuntimeError,
+ value_re='set_network_resources'):
+ child_test.setUpClass()
+
+ def test_set_network_resources_children(self):
+
+ class ChildTest(self.parent_class):
+
+ @classmethod
+ def setup_credentials(cls):
+ cls.set_network_resources(router=True)
+ super(ChildTest, cls).setup_credentials()
+
+ class GrandChildTest(ChildTest):
+ pass
+
+ # Invoke setupClass on both and check that the setup_credentials
+ # call check mechanism does not report any false negative.
+ child_test = ChildTest()
+ child_test.setUpClass()
+ grandchild_test = GrandChildTest()
+ grandchild_test.setUpClass()
+
+
+class TestTempestBaseTestClass(base.TestCase):
+
+ def setUp(self):
+ super(TestTempestBaseTestClass, self).setUp()
+ self.useFixture(fake_config.ConfigFixture())
+ self.patchobject(config, 'TempestConfigPrivate',
+ fake_config.FakePrivate)
+
+ class ParentTest(test.BaseTestCase):
+
+ def runTest(self):
+ pass
+
+ self.parent_test = ParentTest
+
+ def test_resource_cleanup(self):
+ cfg.CONF.set_default('neutron', False, 'service_available')
+ exp_args = (1, 2,)
+ exp_kwargs = {'a': 1, 'b': 2}
+ mock1 = mock.Mock()
+ mock2 = mock.Mock()
+ exp_functions = [mock1, mock2]
+
+ class TestWithCleanups(self.parent_test):
+
+ @classmethod
+ def resource_setup(cls):
+ for fn in exp_functions:
+ cls.addClassResourceCleanup(fn, *exp_args,
+ **exp_kwargs)
+
+ test_cleanups = TestWithCleanups()
+ suite = unittest.TestSuite((test_cleanups,))
+ log = []
+ result = LoggingTestResult(log)
+ suite.run(result)
+ # No exception raised - error log is empty
+ self.assertFalse(log)
+ # All stacked resource cleanups invoked
+ mock1.assert_called_once_with(*exp_args, **exp_kwargs)
+ mock2.assert_called_once_with(*exp_args, **exp_kwargs)
+ # Cleanup stack is empty
+ self.assertEqual(0, len(test_cleanups._class_cleanups))
+
+ def test_resource_cleanup_failures(self):
+ cfg.CONF.set_default('neutron', False, 'service_available')
+ exp_args = (1, 2,)
+ exp_kwargs = {'a': 1, 'b': 2}
+ mock1 = mock.Mock()
+ mock1.side_effect = Exception('mock1 resource cleanup failure')
+ mock2 = mock.Mock()
+ mock3 = mock.Mock()
+ mock3.side_effect = Exception('mock3 resource cleanup failure')
+ exp_functions = [mock1, mock2, mock3]
+
+ class TestWithFailingCleanups(self.parent_test):
+
+ @classmethod
+ def resource_setup(cls):
+ for fn in exp_functions:
+ cls.addClassResourceCleanup(fn, *exp_args,
+ **exp_kwargs)
+
+ test_cleanups = TestWithFailingCleanups()
+ suite = unittest.TestSuite((test_cleanups,))
+ log = []
+ result = LoggingTestResult(log)
+ suite.run(result)
+ # One multiple exception captured
+ self.assertEqual(1, len(log))
+ # [0]: test, err, details [1] -> exc_info
+ # Type, Exception, traceback [1] -> MultipleException
+ found_exc = log[0][1][1]
+ self.assertTrue(isinstance(found_exc, testtools.MultipleExceptions))
+ self.assertEqual(2, len(found_exc.args))
+ # Each arg is exc_info - match messages and order
+ self.assertIn('mock3 resource', str(found_exc.args[0][1]))
+ self.assertIn('mock1 resource', str(found_exc.args[1][1]))
+ # All stacked resource cleanups invoked
+ mock1.assert_called_once_with(*exp_args, **exp_kwargs)
+ mock2.assert_called_once_with(*exp_args, **exp_kwargs)
+ # Cleanup stack is empty
+ self.assertEqual(0, len(test_cleanups._class_cleanups))
+
+ def test_super_resource_cleanup_not_invoked(self):
+
+ class BadResourceCleanup(self.parent_test):
+
+ @classmethod
+ def resource_cleanup(cls):
+ pass
+
+ bad_class = BadResourceCleanup()
+ suite = unittest.TestSuite((bad_class,))
+ log = []
+ result = LoggingTestResult(log)
+ suite.run(result)
+ # One multiple exception captured
+ self.assertEqual(1, len(log))
+ # [0]: test, err, details [1] -> exc_info
+ # Type, Exception, traceback [1] -> RuntimeError
+ found_exc = log[0][1][1]
+ self.assertTrue(isinstance(found_exc, RuntimeError))
+ self.assertIn(BadResourceCleanup.__name__, str(found_exc))
+
+ def test_super_skip_checks_not_invoked(self):
+
+ class BadSkipChecks(self.parent_test):
+
+ @classmethod
+ def skip_checks(cls):
+ pass
+
+ bad_class = BadSkipChecks()
+ with testtools.ExpectedException(
+ RuntimeError,
+ value_re='^.* ' + BadSkipChecks.__name__):
+ bad_class.setUpClass()
+
+ def test_super_setup_credentials_not_invoked(self):
+
+ class BadSetupCredentials(self.parent_test):
+
+ @classmethod
+ def skip_checks(cls):
+ pass
+
+ bad_class = BadSetupCredentials()
+ with testtools.ExpectedException(
+ RuntimeError,
+ value_re='^.* ' + BadSetupCredentials.__name__):
+ bad_class.setUpClass()
+
+ def test_grandparent_skip_checks_not_invoked(self):
+
+ class BadSkipChecks(self.parent_test):
+
+ @classmethod
+ def skip_checks(cls):
+ pass
+
+ class SonOfBadSkipChecks(BadSkipChecks):
+ pass
+
+ bad_class = SonOfBadSkipChecks()
+ with testtools.ExpectedException(
+ RuntimeError,
+ value_re='^.* ' + SonOfBadSkipChecks.__name__):
+ bad_class.setUpClass()
+
+ @mock.patch('tempest.common.credentials_factory.is_admin_available',
+ autospec=True, return_value=True)
+ def test_skip_checks_admin(self, mock_iaa):
+ identity_version = 'identity_version'
+
+ class NeedAdmin(self.parent_test):
+ credentials = ['admin']
+
+ @classmethod
+ def get_identity_version(cls):
+ return identity_version
+
+ NeedAdmin().skip_checks()
+ mock_iaa.assert_called_once_with('identity_version')
+
+ @mock.patch('tempest.common.credentials_factory.is_admin_available',
+ autospec=True, return_value=False)
+ def test_skip_checks_admin_not_available(self, mock_iaa):
+ identity_version = 'identity_version'
+
+ class NeedAdmin(self.parent_test):
+ credentials = ['admin']
+
+ @classmethod
+ def get_identity_version(cls):
+ return identity_version
+
+ with testtools.ExpectedException(testtools.testcase.TestSkipped):
+ NeedAdmin().skip_checks()
+ mock_iaa.assert_called_once_with('identity_version')
+
+ def test_skip_checks_identity_v2_not_available(self):
+ cfg.CONF.set_default('api_v2', False, 'identity-feature-enabled')
+
+ class NeedV2(self.parent_test):
+ identity_version = 'v2'
+
+ with testtools.ExpectedException(testtools.testcase.TestSkipped):
+ NeedV2().skip_checks()
+
+ def test_skip_checks_identity_v3_not_available(self):
+ cfg.CONF.set_default('api_v3', False, 'identity-feature-enabled')
+
+ class NeedV3(self.parent_test):
+ identity_version = 'v3'
+
+ with testtools.ExpectedException(testtools.testcase.TestSkipped):
+ NeedV3().skip_checks()
+
+ def test_setup_credentials_all(self):
+ expected_creds = ['string', ['list', 'role1', 'role2']]
+
+ class AllCredentials(self.parent_test):
+ credentials = expected_creds
+
+ expected_clients = 'clients'
+ with mock.patch.object(
+ AllCredentials,
+ 'get_client_manager') as mock_get_client_manager:
+ mock_get_client_manager.return_value = expected_clients
+ all_creds = AllCredentials()
+ all_creds.setup_credentials()
+ self.assertTrue(hasattr(all_creds, 'os_string'))
+ self.assertEqual(expected_clients, all_creds.os_string)
+ self.assertTrue(hasattr(all_creds, 'os_roles_list'))
+ self.assertEqual(expected_clients, all_creds.os_roles_list)
+ self.assertEqual(2, mock_get_client_manager.call_count)
+ self.assertEqual(
+ expected_creds[0],
+ mock_get_client_manager.mock_calls[0][2]['credential_type'])
+ self.assertEqual(
+ expected_creds[1][1:],
+ mock_get_client_manager.mock_calls[1][2]['roles'])
+
+ def test_setup_class_overwritten(self):
+
+ class OverridesSetup(self.parent_test):
+
+ @classmethod
+ def setUpClass(cls): # noqa
+ pass
+
+ overrides_setup = OverridesSetup()
+ suite = unittest.TestSuite((overrides_setup,))
+ log = []
+ result = LoggingTestResult(log)
+ suite.run(result)
+ # Record 0, test (error holder). The error generates during test run.
+ self.assertIn('runTest', str(log[0][0]))
+ # Record 0, traceback
+ self.assertRegex(
+ str(log[0][2]['traceback']).replace('\n', ' '),
+ RuntimeError.__name__ + ': .* ' + OverridesSetup.__name__)
+
+
+class TestTempestBaseTestClassFixtures(base.TestCase):
+
+ SETUP_FIXTURES = [test.BaseTestCase.setUpClass.__name__,
+ test.BaseTestCase.skip_checks.__name__,
+ test.BaseTestCase.setup_credentials.__name__,
+ test.BaseTestCase.setup_clients.__name__,
+ test.BaseTestCase.resource_setup.__name__]
+ TEARDOWN_FIXTURES = [test.BaseTestCase.tearDownClass.__name__,
+ test.BaseTestCase.resource_cleanup.__name__,
+ test.BaseTestCase.clear_credentials.__name__]
+
+ def setUp(self):
+ super(TestTempestBaseTestClassFixtures, self).setUp()
+ self.mocks = {}
+ for fix in self.SETUP_FIXTURES + self.TEARDOWN_FIXTURES:
+ self.mocks[fix] = mock.Mock()
+
+ def tracker_builder(name):
+
+ def tracker(cls):
+ # Track that the fixture was invoked
+ cls.fixtures_invoked.append(name)
+ # Run the fixture
+ getattr(super(TestWithClassFixtures, cls), name)()
+ # Run a mock we can use for side effects
+ self.mocks[name]()
+
+ return tracker
+
+ class TestWithClassFixtures(test.BaseTestCase):
+
+ credentials = []
+ fixtures_invoked = []
+
+ def runTest(_self):
+ pass
+
+ # Decorate all test class fixtures with tracker_builder
+ for method_name in self.SETUP_FIXTURES + self.TEARDOWN_FIXTURES:
+ setattr(TestWithClassFixtures, method_name,
+ classmethod(tracker_builder(method_name)))
+
+ self.test = TestWithClassFixtures()
+
+ def test_no_error_flow(self):
+ # If all setup fixtures are executed, all cleanup fixtures are
+ # executed too
+ suite = unittest.TestSuite((self.test,))
+ log = []
+ result = LoggingTestResult(log)
+ suite.run(result)
+ self.assertEqual(self.SETUP_FIXTURES + self.TEARDOWN_FIXTURES,
+ self.test.fixtures_invoked)
+
+ def test_skip_only(self):
+ # If a skip condition is hit in the test, no credentials or resource
+ # is provisioned / cleaned-up
+ self.mocks['skip_checks'].side_effect = (
+ testtools.testcase.TestSkipped())
+ suite = unittest.TestSuite((self.test,))
+ log = []
+ result = LoggingTestResult(log)
+ suite.run(result)
+ # If we trigger a skip condition, teardown is not invoked at all
+ self.assertEqual(self.SETUP_FIXTURES[:2],
+ self.test.fixtures_invoked)
+
+ def test_skip_credentials_fails(self):
+ expected_exc = 'sc exploded'
+ self.mocks['setup_credentials'].side_effect = Exception(expected_exc)
+ suite = unittest.TestSuite((self.test,))
+ log = []
+ result = LoggingTestResult(log)
+ suite.run(result)
+ # If setup_credentials explodes, we invoked teardown class and
+ # clear credentials, and re-raise
+ self.assertEqual((self.SETUP_FIXTURES[:3] +
+ [self.TEARDOWN_FIXTURES[i] for i in (0, 2)]),
+ self.test.fixtures_invoked)
+ found_exc = log[0][1][1]
+ self.assertIn(expected_exc, str(found_exc))
+
+ def test_skip_credentials_fails_clear_fails(self):
+ # If cleanup fails on failure, we log the exception and do not
+ # re-raise it. Note that since the exception happens outside of
+ # the Tempest test setUp, logging is not captured on the Tempest
+ # test side, it will be captured by the unit test instead.
+ expected_exc = 'sc exploded'
+ clear_exc = 'clear exploded'
+ self.mocks['setup_credentials'].side_effect = Exception(expected_exc)
+ self.mocks['clear_credentials'].side_effect = Exception(clear_exc)
+ suite = unittest.TestSuite((self.test,))
+ log = []
+ result = LoggingTestResult(log)
+ suite.run(result)
+ # If setup_credentials explodes, we invoked teardown class and
+ # clear credentials, and re-raise
+ self.assertEqual((self.SETUP_FIXTURES[:3] +
+ [self.TEARDOWN_FIXTURES[i] for i in (0, 2)]),
+ self.test.fixtures_invoked)
+ found_exc = log[0][1][1]
+ self.assertIn(expected_exc, str(found_exc))
+ # Since log capture depends on OS_LOG_CAPTURE, we can only assert if
+ # logging was captured
+ if os.environ.get('OS_LOG_CAPTURE'):
+ self.assertIn(clear_exc, self.log_fixture.logger.output)
+
+ def test_skip_credentials_clients_resources_credentials_clear_fails(self):
+ # If cleanup fails with no previous failure, we re-raise the exception.
+ expected_exc = 'clear exploded'
+ self.mocks['clear_credentials'].side_effect = Exception(expected_exc)
+ suite = unittest.TestSuite((self.test,))
+ log = []
+ result = LoggingTestResult(log)
+ suite.run(result)
+ # If setup_credentials explodes, we invoked teardown class and
+ # clear credentials, and re-raise
+ self.assertEqual(self.SETUP_FIXTURES + self.TEARDOWN_FIXTURES,
+ self.test.fixtures_invoked)
+ found_exc = log[0][1][1]
+ self.assertIn(expected_exc, str(found_exc))
+
+ def test_skip_credentials_clients_fails(self):
+ expected_exc = 'clients exploded'
+ self.mocks['setup_clients'].side_effect = Exception(expected_exc)
+ suite = unittest.TestSuite((self.test,))
+ log = []
+ result = LoggingTestResult(log)
+ suite.run(result)
+ # If setup_clients explodes, we invoked teardown class and
+ # clear credentials, and re-raise
+ self.assertEqual((self.SETUP_FIXTURES[:4] +
+ [self.TEARDOWN_FIXTURES[i] for i in (0, 2)]),
+ self.test.fixtures_invoked)
+ found_exc = log[0][1][1]
+ self.assertIn(expected_exc, str(found_exc))
+
+ def test_skip_credentials_clients_resources_fails(self):
+ expected_exc = 'resource setup exploded'
+ self.mocks['resource_setup'].side_effect = Exception(expected_exc)
+ suite = unittest.TestSuite((self.test,))
+ log = []
+ result = LoggingTestResult(log)
+ suite.run(result)
+ # If resource_setup explodes, we invoked teardown class and
+ # clear credentials and resource cleanup, and re-raise
+ self.assertEqual(self.SETUP_FIXTURES + self.TEARDOWN_FIXTURES,
+ self.test.fixtures_invoked)
+ found_exc = log[0][1][1]
+ self.assertIn(expected_exc, str(found_exc))
diff --git a/test-requirements.txt b/test-requirements.txt
index 29f0865..37644d0 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -4,7 +4,7 @@
hacking!=0.13.0,<0.14,>=0.12.0 # Apache-2.0
# needed for doc build
sphinx>=1.6.2 # BSD
-openstackdocstheme>=1.16.0 # Apache-2.0
+openstackdocstheme>=1.17.0 # Apache-2.0
reno>=2.5.0 # Apache-2.0
mock>=2.0.0 # BSD
coverage!=4.4,>=4.0 # Apache-2.0
diff --git a/tools/generate-tempest-plugins-list.py b/tools/generate-tempest-plugins-list.py
index 99df0d1..dd05438 100644
--- a/tools/generate-tempest-plugins-list.py
+++ b/tools/generate-tempest-plugins-list.py
@@ -28,12 +28,12 @@
try:
# For Python 3.0 and later
- from urllib.error import HTTPError as HTTPError
+ from urllib.error import HTTPError
import urllib.request as urllib
except ImportError:
# Fall back to Python 2's urllib2
import urllib2 as urllib
- from urllib2 import HTTPError as HTTPError
+ from urllib2 import HTTPError
url = 'https://review.openstack.org/projects/'
diff --git a/tools/generate-tempest-plugins-list.sh b/tools/generate-tempest-plugins-list.sh
index e6aad86..20c99b2 100755
--- a/tools/generate-tempest-plugins-list.sh
+++ b/tools/generate-tempest-plugins-list.sh
@@ -33,8 +33,8 @@
# * network access to https://git.openstack.org/cgit
# ))
#
-# If a file named data/tempest-plugins-registry.header or
-# data/tempest-plugins-registry.footer is found relative to the
+# If a file named doc/source/data/tempest-plugins-registry.header or
+# doc/source/data/tempest-plugins-registry.footer is found relative to the
# current working directory, it will be prepended or appended to
# the generated reStructuredText plugins table respectively.
@@ -43,8 +43,8 @@
(
declare -A plugins
-if [[ -r data/tempest-plugins-registry.header ]]; then
- cat data/tempest-plugins-registry.header
+if [[ -r doc/source/data/tempest-plugins-registry.header ]]; then
+ cat doc/source/data/tempest-plugins-registry.header
fi
sorted_plugins=$(python tools/generate-tempest-plugins-list.py)
@@ -56,8 +56,8 @@
printf "+----------------------------+-------------------------------------------------------------------------+\n"
done
-if [[ -r data/tempest-plugins-registry.footer ]]; then
- cat data/tempest-plugins-registry.footer
+if [[ -r doc/source/data/tempest-plugins-registry.footer ]]; then
+ cat doc/source/data/tempest-plugins-registry.footer
fi
) > doc/source/plugin-registry.rst
diff --git a/tools/tempest-plugin-sanity.sh b/tools/tempest-plugin-sanity.sh
index a4f706e..8b4f913 100644
--- a/tools/tempest-plugin-sanity.sh
+++ b/tools/tempest-plugin-sanity.sh
@@ -20,7 +20,7 @@
# What it does:
# * Creates the virtualenv
# * Install tempest
-# * Retrive the project lists having tempest plugin if project name is
+# * Retrieve the project lists having tempest plugin if project name is
# given.
# * For each project in a list, It does:
# * Clone the Project
@@ -120,3 +120,8 @@
failed_plugin+=", $project"
fi
done
+
+# Check for failed status
+if [[ -n $failed_plugin ]]; then
+ exit 1
+fi
diff --git a/tox.ini b/tox.ini
index 24e3b5d..db5976f 100644
--- a/tox.ini
+++ b/tox.ini
@@ -16,11 +16,11 @@
[testenv]
setenv =
VIRTUAL_ENV={envdir}
- OS_TEST_PATH=./tempest/tests
+ OS_LOG_CAPTURE=1
PYTHONWARNINGS=default::DeprecationWarning
BRANCH_NAME=master
CLIENT_NAME=tempest
-passenv = OS_STDOUT_CAPTURE OS_STDERR_CAPTURE OS_TEST_TIMEOUT OS_TEST_LOCK_PATH OS_TEST_PATH TEMPEST_CONFIG TEMPEST_CONFIG_DIR http_proxy HTTP_PROXY https_proxy HTTPS_PROXY no_proxy NO_PROXY ZUUL_CACHE_DIR REQUIREMENTS_PIP_LOCATION GENERATE_TEMPEST_PLUGIN_LIST
+passenv = OS_STDOUT_CAPTURE OS_STDERR_CAPTURE OS_TEST_TIMEOUT OS_TEST_LOCK_PATH TEMPEST_CONFIG TEMPEST_CONFIG_DIR http_proxy HTTP_PROXY https_proxy HTTPS_PROXY no_proxy NO_PROXY ZUUL_CACHE_DIR REQUIREMENTS_PIP_LOCATION GENERATE_TEMPEST_PLUGIN_LIST
usedevelop = True
install_command =
{toxinidir}/tools/tox_install.sh {env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} {opts} {packages}
@@ -30,13 +30,23 @@
-r{toxinidir}/test-requirements.txt
commands =
find . -type f -name "*.pyc" -delete
- ostestr {posargs}
+ stestr --test-path ./tempest/tests run {posargs}
[testenv:genconfig]
commands = oslo-config-generator --config-file tempest/cmd/config-generator.tempest.conf
[testenv:cover]
-commands = python setup.py testr --coverage --testr-arg='tempest\.tests {posargs}'
+setenv =
+ {[testenv]setenv}
+ PYTHON=coverage run --source tempest --parallel-mode
+commands =
+ coverage erase
+ find . -type f -name "*.pyc" -delete
+ stestr --test-path ./tempest/tests run {posargs}
+ coverage combine
+ coverage html -d cover
+ coverage xml -o cover/coverage.xml
+ coverage report
[testenv:all]
envdir = .tox/tempest
@@ -50,17 +60,6 @@
find . -type f -name "*.pyc" -delete
tempest run --regex {posargs}
-[testenv:ostestr]
-sitepackages = {[tempestenv]sitepackages}
-# 'all' includes slow tests
-setenv =
- {[tempestenv]setenv}
- OS_TEST_TIMEOUT={env:OS_TEST_TIMEOUT:1200}
-deps = {[tempestenv]deps}
-commands =
- find . -type f -name "*.pyc" -delete
- ostestr {posargs}
-
[testenv:all-plugin]
sitepackages = True
# 'all' includes slow tests