11 KiB
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:
image:
regex: cirros-.*-disk
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 inrally.plugins
. Technically, a user could implement a subclass ofrally.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 therally.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:
@types.set(image=types.ImageResourceType,
=types.FlavorResourceType) flavor
...will change to:
@types.convert(image={"type": "glance_image"},
={"type": "nova_flavor"}) 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:
def transform(cls, clients, resource_config):
...it will be:
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:
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:
= clients.nova()
novaclient self._flavor_id = _id_from_name(
=resource_config,
resource_config=novaclient.flavors.list(),
resources="flavor")
typename
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 = {}
= clients.glance().images.list()
all_images 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=self._images[scenario_context["user"]],
resources="image") typename
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()
toResourceType.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 toImageResourceType
andEC2ImageResourceType
. 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.