diff --git a/doc/specs/in-progress/pluggable-types.rst b/doc/specs/in-progress/pluggable-types.rst new file mode 100644 index 00000000..291edd38 --- /dev/null +++ b/doc/specs/in-progress/pluggable-types.rst @@ -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.