e9241b5d89
In "While there are no hard and fast rules for the structure a plugin" a preposition is missing between "structure" and "a plugin", this is to fix it. Change-Id: Ib22f614dece35d2ed79fa660027e50840e77d7bb
350 lines
14 KiB
ReStructuredText
350 lines
14 KiB
ReStructuredText
.. _tempest_plugin:
|
|
|
|
=============================
|
|
Tempest Test Plugin Interface
|
|
=============================
|
|
|
|
Tempest has an external test plugin interface which enables anyone to integrate
|
|
an external test suite as part of a tempest run. This will let any project
|
|
leverage being run with the rest of the tempest suite while not requiring the
|
|
tests live in the tempest tree.
|
|
|
|
Creating a plugin
|
|
=================
|
|
|
|
Creating a plugin is fairly straightforward and doesn't require much additional
|
|
effort on top of creating a test suite using tempest.lib. One thing to note with
|
|
doing this is that the interfaces exposed by tempest are not considered stable
|
|
(with the exception of configuration variables which ever effort goes into
|
|
ensuring backwards compatibility). You should not need to import anything from
|
|
tempest itself except where explicitly noted.
|
|
|
|
Stable Tempest APIs plugins may use
|
|
-----------------------------------
|
|
|
|
As noted above, several tempest APIs are acceptable to use from plugins, while
|
|
others are not. A list of stable APIs available to plugins is provided below:
|
|
|
|
* tempest.lib.*
|
|
* 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
|
|
that situation, file a bug, push a migration patch, etc. to expedite providing
|
|
the interface in a reliable manner.
|
|
|
|
Plugin Cookiecutter
|
|
-------------------
|
|
|
|
In order to create the basic structure with base classes and test directories
|
|
you can use the tempest-plugin-cookiecutter project::
|
|
|
|
> pip install -U cookiecutter && cookiecutter https://git.openstack.org/openstack/tempest-plugin-cookiecutter
|
|
|
|
Cloning into 'tempest-plugin-cookiecutter'...
|
|
remote: Counting objects: 17, done.
|
|
remote: Compressing objects: 100% (13/13), done.
|
|
remote: Total 17 (delta 1), reused 14 (delta 1)
|
|
Unpacking objects: 100% (17/17), done.
|
|
Checking connectivity... done.
|
|
project (default is "sample")? foo
|
|
testclass (default is "SampleTempestPlugin")? FooTempestPlugin
|
|
|
|
This would create a folder called ``foo_tempest_plugin/`` with all necessary
|
|
basic classes. You only need to move/create your test in
|
|
``foo_tempest_plugin/tests``.
|
|
|
|
Entry Point
|
|
-----------
|
|
|
|
Once you've created your plugin class you need to add an entry point to your
|
|
project to enable tempest to find the plugin. The entry point must be added
|
|
to the "tempest.test_plugins" namespace.
|
|
|
|
If you are using pbr this is fairly straightforward, in the setup.cfg just add
|
|
something like the following:
|
|
|
|
.. code-block:: ini
|
|
|
|
[entry_points]
|
|
tempest.test_plugins =
|
|
plugin_name = module.path:PluginClass
|
|
|
|
Standalone Plugin vs In-repo Plugin
|
|
-----------------------------------
|
|
|
|
Since all that's required for a plugin to be detected by tempest is a valid
|
|
setuptools entry point in the proper namespace there is no difference from the
|
|
tempest perspective on either creating a separate python package to
|
|
house the plugin or adding the code to an existing python project. However,
|
|
there are tradeoffs to consider when deciding which approach to take when
|
|
creating a new plugin.
|
|
|
|
If you create a separate python project for your plugin this makes a lot of
|
|
things much easier. Firstly it makes packaging and versioning much simpler, you
|
|
can easily decouple the requirements for the plugin from the requirements for
|
|
the other project. It lets you version the plugin independently and maintain a
|
|
single version of the test code across project release boundaries (see the
|
|
`Branchless Tempest Spec`_ for more details on this). It also greatly
|
|
simplifies the install time story for external users. Instead of having to
|
|
install the right version of a project in the same python namespace as tempest
|
|
they simply need to pip install the plugin in that namespace. It also means
|
|
that users don't have to worry about inadvertently installing a tempest plugin
|
|
when they install another package.
|
|
|
|
.. _Branchless Tempest Spec: http://specs.openstack.org/openstack/qa-specs/specs/tempest/implemented/branchless-tempest.html
|
|
|
|
The sole advantage to integrating a plugin into an existing python project is
|
|
that it enables you to land code changes at the same time you land test changes
|
|
in the plugin. This reduces some of the burden on contributors by not having
|
|
to land 2 changes to add a new API feature and then test it and doing it as a
|
|
single combined commit.
|
|
|
|
|
|
Plugin Class
|
|
============
|
|
|
|
To provide tempest with all the required information it needs to be able to run
|
|
your plugin you need to create a plugin class which tempest will load and call
|
|
to get information when it needs. To simplify creating this tempest provides an
|
|
abstract class that should be used as the parent for your plugin. To use this
|
|
you would do something like the following:
|
|
|
|
.. code-block:: python
|
|
|
|
from tempest.test_discover import plugins
|
|
|
|
class MyPlugin(plugins.TempestPlugin):
|
|
|
|
Then you need to ensure you locally define all of the mandatory methods in the
|
|
abstract class, you can refer to the api doc below for a reference of what that
|
|
entails.
|
|
|
|
Abstract Plugin Class
|
|
---------------------
|
|
|
|
.. autoclass:: tempest.test_discover.plugins.TempestPlugin
|
|
:members:
|
|
|
|
Plugin Structure
|
|
================
|
|
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
|
|
create a directory structure with something like::
|
|
|
|
plugin_dir/
|
|
config.py
|
|
plugin.py
|
|
tests/
|
|
api/
|
|
scenario/
|
|
services/
|
|
client.py
|
|
|
|
That will mirror what people expect from tempest. The file
|
|
|
|
* **config.py**: contains any plugin specific configuration variables
|
|
* **plugin.py**: contains the plugin class used for the entry point
|
|
* **tests**: the directory where test discovery will be run, all tests should
|
|
be under this dir
|
|
* **services**: where the plugin specific service clients are
|
|
|
|
Additionally, when you're creating the plugin you likely want to follow all
|
|
of the tempest developer and reviewer documentation to ensure that the tests
|
|
being added in the plugin act and behave like the rest of tempest.
|
|
|
|
Dealing with configuration options
|
|
----------------------------------
|
|
|
|
Historically Tempest didn't provide external guarantees on its configuration
|
|
options. However, with the introduction of the plugin interface this is no
|
|
longer the case. An external plugin can rely on using any configuration option
|
|
coming from Tempest, there will be at least a full deprecation cycle for any
|
|
option before it's removed. However, just the options provided by Tempest
|
|
may not be sufficient for the plugin. If you need to add any plugin specific
|
|
configuration options you should use the ``register_opts`` and
|
|
``get_opt_lists`` methods to pass them to Tempest when the plugin is loaded.
|
|
When adding configuration options the ``register_opts`` method gets passed the
|
|
CONF object from tempest. This enables the plugin to add options to both
|
|
existing sections and also create new configuration sections for new options.
|
|
|
|
Service Clients
|
|
---------------
|
|
|
|
If a plugin defines a service client, it is beneficial for it to implement the
|
|
``get_service_clients`` method in the plugin class. All service clients which
|
|
are exposed via this interface will be automatically configured and be
|
|
available in any instance of the service clients class, defined in
|
|
``tempest.lib.services.clients.ServiceClients``. In case multiple plugins are
|
|
installed, all service clients from all plugins will be registered, making it
|
|
easy to write tests which rely on multiple APIs whose service clients are in
|
|
different plugins.
|
|
|
|
Example implementation of ``get_service_clients``:
|
|
|
|
.. code-block:: python
|
|
|
|
def get_service_clients(self):
|
|
# Example implementation with two service clients
|
|
my_service1_config = config.service_client_config('my_service')
|
|
params_my_service1 = {
|
|
'name': 'my_service_v1',
|
|
'service_version': 'my_service.v1',
|
|
'module_path': 'plugin_tempest_tests.services.my_service.v1',
|
|
'client_names': ['API1Client', 'API2Client'],
|
|
}
|
|
params_my_service1.update(my_service_config)
|
|
my_service2_config = config.service_client_config('my_service')
|
|
params_my_service2 = {
|
|
'name': 'my_service_v2',
|
|
'service_version': 'my_service.v2',
|
|
'module_path': 'plugin_tempest_tests.services.my_service.v2',
|
|
'client_names': ['API1Client', 'API2Client'],
|
|
}
|
|
params_my_service2.update(my_service2_config)
|
|
return [params_my_service1, params_my_service2]
|
|
|
|
Parameters:
|
|
|
|
* **name**: Name of the attribute used to access the ``ClientsFactory`` from
|
|
the ``ServiceClients`` instance. See example below.
|
|
* **service_version**: Tempest enforces a single implementation for each
|
|
service client. Available service clients are held in a ``ClientsRegistry``
|
|
singleton, and registered with ``service_version``, which means that
|
|
``service_version`` must be unique and it should represent the service API
|
|
and version implemented by the service client.
|
|
* **module_path**: Relative to the service client module, from the root of the
|
|
plugin.
|
|
* **client_names**: Name of the classes that implement service clients in the
|
|
service clients module.
|
|
|
|
Example usage of the service clients in tests:
|
|
|
|
.. code-block:: python
|
|
|
|
# my_creds is instance of tempest.lib.auth.Credentials
|
|
# identity_uri is v2 or v3 depending on the configuration
|
|
from tempest.lib.services import clients
|
|
|
|
my_clients = clients.ServiceClients(my_creds, identity_uri)
|
|
my_service1_api1_client = my_clients.my_service_v1.API1Client()
|
|
my_service2_api1_client = my_clients.my_service_v2.API1Client(my_args='any')
|
|
|
|
Automatic configuration and registration of service clients imposes some extra
|
|
constraints on the structure of the configuration options exposed by the
|
|
plugin.
|
|
|
|
First ``service_version`` should be in the format `service_config[.version]`.
|
|
The `.version` part is optional, and should only be used if there are multiple
|
|
versions of the same API available. The `service_config` must match the name of
|
|
a configuration options group defined by the plugin. Different versions of one
|
|
API must share the same configuration group.
|
|
|
|
Second the configuration options group `service_config` must contain the
|
|
following options:
|
|
|
|
* `catalog_type`: corresponds to `service` in the catalog
|
|
* `endpoint_type`
|
|
|
|
The following options will be honoured if defined, but they are not mandatory,
|
|
as they do not necessarily apply to all service clients.
|
|
|
|
* `region`: default to identity.region
|
|
* `build_timeout` : default to compute.build_timeout
|
|
* `build_interval`: default to compute.build_interval
|
|
|
|
Third the service client classes should inherit from ``RestClient``, should
|
|
accept generic keyword arguments, and should pass those arguments to the
|
|
``__init__`` method of ``RestClient``. Extra arguments can be added. For
|
|
instance:
|
|
|
|
.. code-block:: python
|
|
|
|
class MyAPIClient(rest_client.RestClient):
|
|
|
|
def __init__(self, auth_provider, service, region,
|
|
my_arg, my_arg2=True, **kwargs):
|
|
super(MyAPIClient, self).__init__(
|
|
auth_provider, service, region, **kwargs)
|
|
self.my_arg = my_arg
|
|
self.my_args2 = my_arg
|
|
|
|
Finally the service client should be structured in a python module, so that all
|
|
service client classes are importable from it. Each major API version should
|
|
have its own module.
|
|
|
|
The following folder and module structure is recommended for a single major
|
|
API version::
|
|
|
|
plugin_dir/
|
|
services/
|
|
__init__.py
|
|
client_api_1.py
|
|
client_api_2.py
|
|
|
|
The content of __init__.py module should be:
|
|
|
|
.. code-block:: python
|
|
|
|
from client_api_1.py import API1Client
|
|
from client_api_2.py import API2Client
|
|
|
|
__all__ = ['API1Client', 'API2Client']
|
|
|
|
The following folder and module structure is recommended for multiple major
|
|
API version::
|
|
|
|
plugin_dir/
|
|
services/
|
|
v1/
|
|
__init__.py
|
|
client_api_1.py
|
|
client_api_2.py
|
|
v2/
|
|
__init__.py
|
|
client_api_1.py
|
|
client_api_2.py
|
|
|
|
The content each of __init__.py module under vN should be:
|
|
|
|
.. code-block:: python
|
|
|
|
from client_api_1.py import API1Client
|
|
from client_api_2.py import API2Client
|
|
|
|
__all__ = ['API1Client', 'API2Client']
|
|
|
|
Using Plugins
|
|
=============
|
|
|
|
Tempest will automatically discover any installed plugins when it is run. So by
|
|
just installing the python packages which contain your plugin you'll be using
|
|
them with tempest, nothing else is really required.
|
|
|
|
However, you should take care when installing plugins. By their very nature
|
|
there are no guarantees when running tempest with plugins enabled about the
|
|
quality of the plugin. Additionally, while there is no limitation on running
|
|
with multiple plugins it's worth noting that poorly written plugins might not
|
|
properly isolate their tests which could cause unexpected cross interactions
|
|
between plugins.
|
|
|
|
Notes for using plugins with virtualenvs
|
|
----------------------------------------
|
|
|
|
When using a tempest inside a virtualenv (like when running under tox) you have
|
|
to ensure that the package that contains your plugin is either installed in the
|
|
venv too or that you have system site-packages enabled. The virtualenv will
|
|
isolate the tempest install from the rest of your system so just installing the
|
|
plugin package on your system and then running tempest inside a venv will not
|
|
work.
|
|
|
|
Tempest also exposes a tox job, all-plugin, which will setup a tox virtualenv
|
|
with system site-packages enabled. This will let you leverage tox without
|
|
requiring to manually install plugins in the tox venv before running tests.
|