Merge "[spec] Improve and make pluggable type conversion"
This commit is contained in:
commit
4945b446b0
286
doc/specs/in-progress/pluggable-types.rst
Normal file
286
doc/specs/in-progress/pluggable-types.rst
Normal file
@ -0,0 +1,286 @@
|
||||
..
|
||||
This work is licensed under a Creative Commons Attribution 3.0 Unported
|
||||
License.
|
||||
|
||||
http://creativecommons.org/licenses/by/3.0/legalcode
|
||||
|
||||
=============================
|
||||
Make Resource Types Pluggable
|
||||
=============================
|
||||
|
||||
Rally's current type resolution subsystem (``rally.task.types``) isn't
|
||||
easily pluggable, is tied to OpenStack, and cannot handle resources
|
||||
that must be resolved by the context of each iteration rather than the
|
||||
context of the subtask. This spec aims to solve all three problems.
|
||||
|
||||
Problem description
|
||||
===================
|
||||
|
||||
The Rally ``types.set()`` decorator is used to convert resource names
|
||||
or regular expressions to resource objects. For instance, in a task
|
||||
file a user can specify:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
image:
|
||||
regex: cirros-.*-uec
|
||||
flavor:
|
||||
name: m1.tiny
|
||||
|
||||
Rally will convert those into the matching image ID and flavor ID. As
|
||||
it currently exists, this process has several shortcomings and bugs:
|
||||
|
||||
* Although it is technically pluggable, the resource type classes do
|
||||
not call ``rally.common.plugin.configure`` and the code is not
|
||||
patterned as a plugin, with code in ``rally.plugins``. Technically,
|
||||
a user could implement a subclass of
|
||||
``rally.task.types.ResourceType`` in a plugin and use it, but this
|
||||
is not obvious from the code or documentation, and it would not be
|
||||
registered as a plugin. Moreover, OpenStack-specific resources are
|
||||
in the ``rally.task.types`` module instead of the OpenStack plugin.
|
||||
* It is tied to OpenStack. ``rally.task.types.preprocess()`` loads an
|
||||
OpenStack Clients object and passes it to the resource type objects.
|
||||
* In some cases, resources must be loaded by the client context
|
||||
created for each iteration, not by the admin context. For instance,
|
||||
when Glance images are created by the ``images`` context they are
|
||||
created as private images in each tenant; trying to load the image
|
||||
with the admin context fails. We need to be able to support this use
|
||||
case, without taking on a significant or universal performance
|
||||
penalty.
|
||||
|
||||
Proposed change
|
||||
===============
|
||||
|
||||
This change is very involved and is broken into a number of distinct
|
||||
sections.
|
||||
|
||||
Create ``types.convert()``
|
||||
--------------------------
|
||||
|
||||
First, we will add a new function, ``types.convert()``, to replace
|
||||
``types.set()``. ``types.convert()`` will accept arguments differently
|
||||
than ``types.set()``. For instance, this:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@types.set(image=types.ImageResourceType,
|
||||
flavor=types.FlavorResourceType)
|
||||
|
||||
...will change to:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@types.convert(image={"type": "glance_image"},
|
||||
flavor={"type": "nova_flavor"})
|
||||
|
||||
This has a number of advantages:
|
||||
|
||||
* Resource type classes can be named or removed, or the interface
|
||||
changed, without breaking the public API.
|
||||
* Users will not have to import types in the code. Currently this is
|
||||
only a single module, but this spec proposes to change that.
|
||||
* Plugins are loaded automatically, rather than users having to import
|
||||
them explicitly.
|
||||
* We can use the existing plugin deprecation mechanisms.
|
||||
* By passing a dict to ``types.convert()`` instead of a class, we
|
||||
could in theory pass arguments to the types. Nothing in this spec
|
||||
requires that functionality, but it is provided for the future.
|
||||
* ``set`` is a reserved keyword, so by renaming the function we
|
||||
eliminate a bit of code that is in violation of the OpenStack Style
|
||||
Guidelines.
|
||||
|
||||
Convert ``ResourceType`` to plugin
|
||||
----------------------------------
|
||||
|
||||
Next, the code will be rearranged to make it obviously pluggable,
|
||||
and a ``types.configure()`` call will be added to register the
|
||||
``ResourceType`` subclasses as plugins. OpenStack resources will be
|
||||
moved into the OpenStack plugin space, and documentation will be added
|
||||
to make it clear that ``ResourceType`` can be subclassed by other
|
||||
plugins. The old resource type classes will be left in place, but
|
||||
deprecated. ``types.set()`` will also be deprecated at this point.
|
||||
|
||||
Switch scenarios to ``types.convert()`` and new type plugins
|
||||
------------------------------------------------------------
|
||||
|
||||
After resource type plugins are created, all existing scenarios will
|
||||
be changed over to ``types.convert()``. This will allow us to make the
|
||||
changes below that affect the type conversion API without having to
|
||||
make further changes to the scenarios.
|
||||
|
||||
Change type preprocessing signature
|
||||
-----------------------------------
|
||||
|
||||
The arguments with which each preprocessor is called will be
|
||||
changed. Instead of:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def transform(cls, clients, resource_config):
|
||||
|
||||
...it will be:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def preprocess(self, resource_config, context=None, clients=None):
|
||||
|
||||
Within the types subsystem proper, only ``context`` will be passed;
|
||||
``clients`` will remain for compatibility with the validation
|
||||
subsystem, which does not have a context object yet, and remains tied
|
||||
to OpenStack.
|
||||
|
||||
If ``clients`` is not passed to ``transform()``, the responsibility
|
||||
for creating OpenStack clients (or doing anything else with the
|
||||
subtask context) will lie with the ``ResourceType`` subclass
|
||||
itself. This entails a small performance penalty, but it's necessary
|
||||
to divorce the types subsystem from OpenStack. If ``clients`` is
|
||||
passed, then a deprecation warning will be logged. When the validation
|
||||
subsystem is made independent from OpenStack, the ``clients`` keyword
|
||||
argument should be removed.
|
||||
|
||||
This also makes it so that ``ResourceType.transform()`` is no longer a
|
||||
class method, which will allow the resource classes to retain
|
||||
persistent information about a single decorated scenario
|
||||
function. ``transform`` will also be renamed to ``preprocess``, which
|
||||
will be more consistent with ``rally.task.types.preprocess`` and will
|
||||
make it easier to add a second resource type resolution hook,
|
||||
described below.
|
||||
|
||||
Add ``ResourceType.map_for_scenario()``
|
||||
---------------------------------------
|
||||
|
||||
A new hook will be added to the runners. In addition to
|
||||
``ResourceType.preprocess()``, which is run after contexts but before
|
||||
the scenarios start, ``ResourceType.map_for_scenario(self,
|
||||
scenario_context, resource_config)`` will run before each iteration of the
|
||||
scenario. Together with the change to make ``types.set()`` accept
|
||||
objects instead of classes, this will solve the issue of resources
|
||||
that must be resolved per-iteration.
|
||||
|
||||
For instance, to resolve images, ``ImageResourceType.preprocess()``
|
||||
would resolve images for each set of credentials created for the
|
||||
subtask, as well as for the admin credentials, and cache them;
|
||||
``ImageResourceType.map_for_scenario()`` would be passed the mapped
|
||||
scenario context and the resource configuration, and would choose the
|
||||
correct image ID to pass to the scenario. Note that image listing and
|
||||
resolution is not done by ``map_for_scenario()``; we should strive to
|
||||
keep the performance profile of ``map_for_scenario()`` as small as
|
||||
possible.
|
||||
|
||||
In order to simplify the type resolution workflow, only
|
||||
``map_for_scenario()`` will be able to rewrite arguments, but the
|
||||
default implementation in ``rally.task.types.ResourceType`` will
|
||||
rewrite it with the value cached in ``preprocess()``. For instance:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class ResourceType(plugin.Plugin):
|
||||
|
||||
@abc.abstractmethod
|
||||
def preprocess(self, context, resource_config):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def map_for_scenario(self, scenario_context, resource_config):
|
||||
pass
|
||||
|
||||
|
||||
class FlavorResourceType(ResourceType):
|
||||
def preprocess(self, resource_config, context=None, clients=None):
|
||||
self._flavor_id = resource_config.get("id")
|
||||
if not self._flavor_id:
|
||||
novaclient = clients.nova()
|
||||
self._flavor_id = _id_from_name(
|
||||
resource_config=resource_config,
|
||||
resources=novaclient.flavors.list(),
|
||||
typename="flavor")
|
||||
|
||||
def map_for_scenario(self, scenario_context, resource_config):
|
||||
return self._flavor_id
|
||||
|
||||
|
||||
class ImageResourceType(ResourceType):
|
||||
def preprocess(self, resource_config, context=None, clients=None):
|
||||
self._image_id = resource_config.get("id")
|
||||
if not self._image_id:
|
||||
self._images = {}
|
||||
all_images = clients.glance().images.list()
|
||||
for image in all_images:
|
||||
self._images.setdefault(image["owner"], []).append(image)
|
||||
|
||||
def map_for_scenario(self, scenario_context, resource_config):
|
||||
if self._image_id:
|
||||
return self._image_id
|
||||
else:
|
||||
return _id_from_name(
|
||||
resource_config=resource_config,
|
||||
resources=self._images[scenario_context["user"]],
|
||||
typename="image")
|
||||
|
||||
This demonstrates two different workflows.
|
||||
|
||||
Flavors, which exist globally for all users and tenants, can be easily
|
||||
resolved once, at preprocessing time, and ``map_for_scenario()`` needs
|
||||
only to substitute the single, canonical flavor ID on each
|
||||
iteration. This does lead to some redundancy -- flavor arguments will
|
||||
be rewritten on each iteration, for instance -- but as it's only a
|
||||
matter of changing a few values in the argument dict, the performance
|
||||
penalty will be minimal.
|
||||
|
||||
Images are more complicated, because images can exist on a per-user
|
||||
basis, and remain invisible to other users. In order to properly
|
||||
resolve image IDs, we must first find all images in ``preprocess()``,
|
||||
and then select the correct image for each iteration (and for the user
|
||||
that maps to each iteration) in ``map_for_scenario()``.
|
||||
|
||||
Remove deprecated code
|
||||
----------------------
|
||||
|
||||
Finally, in some future release we will remove the old, deprecated
|
||||
resource type classes and ``types.set()``.
|
||||
|
||||
Alternatives
|
||||
------------
|
||||
|
||||
Type resolution could be done in a single step (as opposed to the two
|
||||
step ``preprocess()``/``map_for_scenario()``) if we passed the results in
|
||||
the context object instead of rewriting scenario arguments. This is
|
||||
less straightforward, though; the scenario author would then need to
|
||||
know where to look in the context to find the resource object, even
|
||||
though for any given iteration there is exactly one resource object
|
||||
that is appropriate.
|
||||
|
||||
Implementation
|
||||
==============
|
||||
|
||||
Assignee(s)
|
||||
-----------
|
||||
|
||||
Primary assignee:
|
||||
stpierre aka Chris St. Pierre
|
||||
|
||||
Work Items
|
||||
----------
|
||||
|
||||
* Create ``types.convert()``.
|
||||
* Rearrange the code into plugins and add plugin
|
||||
documentation. Deprecate ``types.set()`` and the old resource type
|
||||
classes.
|
||||
* Convert existing scenarios to use ``types.convert()``.
|
||||
* Convert ``ResourceType.transform()`` to
|
||||
``ResourceType.preprocess()`` and create a new abstract intermediate
|
||||
subclass, ``OpenStackResourceType``, to which to offload OpenStack
|
||||
client creation.
|
||||
* Add the ``ResourceType.map_for_scenario()`` hook.
|
||||
* Rewrite any resource types that need to take advantage of the new
|
||||
``map_for_scenario()`` hook. This will likely be limited to
|
||||
``ImageResourceType`` and ``EC2ImageResourceType``. If there are
|
||||
obvious patterns that can be abstracted out, then add a new abstract
|
||||
intermediate subclass.
|
||||
* In the indeterminate future, remove the deprecated resource type
|
||||
classes and ``types.set()``.
|
||||
|
||||
Dependencies
|
||||
============
|
||||
|
||||
None.
|
Loading…
Reference in New Issue
Block a user