Merge "[spec] Refactoring scenario utils"
This commit is contained in:
commit
91572dca9e
357
doc/specs/in-progress/refactor_scenario_utils.rst
Normal file
357
doc/specs/in-progress/refactor_scenario_utils.rst
Normal file
@ -0,0 +1,357 @@
|
||||
..
|
||||
This work is licensed under a Creative Commons Attribution 3.0 Unported
|
||||
License.
|
||||
|
||||
http://creativecommons.org/licenses/by/3.0/legalcode
|
||||
|
||||
|
||||
=========================================================
|
||||
Refactor scenarios' utils into central os-services tree
|
||||
=========================================================
|
||||
|
||||
It's hard to reuse code from different scenario utils in areas like context.
|
||||
|
||||
|
||||
Problem description
|
||||
===================
|
||||
|
||||
* Code that wraps openstack services from different scenario utils is
|
||||
difficult to reuse in context plugins (or, sometimes in different scenarios
|
||||
plugins), which causes code duplications.
|
||||
|
||||
* Wrappers don't fully integrate with the current structure (example: network
|
||||
operations need to alternate between calls to utils and calls to network
|
||||
wrappers).
|
||||
|
||||
* It is impossible to do versioning of current utils which makes them
|
||||
hard to reuse as a base for out of tree plugins.
|
||||
|
||||
* Is is not possible to have separated common functionality (e.g. network) and
|
||||
specific implementation features (nova network and neutron)
|
||||
|
||||
|
||||
Proposed change
|
||||
===============
|
||||
|
||||
Group all service related utils under a single tree accessible from all areas
|
||||
of the project.
|
||||
Also, inheritance structure in scenarios is problematic. This would be a great
|
||||
opportunity to move to composition.
|
||||
|
||||
Alternatives
|
||||
------------
|
||||
|
||||
None comes to mind.
|
||||
|
||||
Implementation
|
||||
==============
|
||||
|
||||
Current source tree
|
||||
-------------------
|
||||
|
||||
.. code-block::
|
||||
|
||||
rally/
|
||||
|
|
||||
+-- plugins/
|
||||
+-- openstack/
|
||||
| +-- scenarios/
|
||||
| | |
|
||||
| | +-- nova/
|
||||
| | | |
|
||||
| | | +-- servers.py
|
||||
| | | |
|
||||
| | | +-- utils.py
|
||||
| | |
|
||||
| | +-- ...
|
||||
| +-- wrappers/
|
||||
| |
|
||||
| +-- keystone.py
|
||||
| |
|
||||
| +-- network.py
|
||||
|
||||
keystone scenarios use plugins/openstack/scenarios/keystone/utils.py
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@atomic.action_timer("keystone.create_tenant")
|
||||
def _tenant_create(self, name_length=10, **kwargs):
|
||||
"""Creates keystone tenant with random name.
|
||||
|
||||
:param name_length: length of generated (random) part of name
|
||||
:param kwargs: Other optional parameters
|
||||
:returns: keystone tenant instance
|
||||
"""
|
||||
name = self._generate_random_name(length=name_length)
|
||||
return self.admin_clients("keystone").tenants.create(name, **kwargs)
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class KeystoneBasic(kutils.KeystoneScenario):
|
||||
"""Basic benchmark scenarios for Keystone."""
|
||||
|
||||
@validation.number("name_length", minval=10)
|
||||
@validation.required_openstack(admin=True)
|
||||
@scenario.configure(context={"admin_cleanup": ["keystone"]})
|
||||
def create_tenant(self, name_length=10, **kwargs):
|
||||
"""Create a keystone tenant with random name.
|
||||
|
||||
:param name_length: length of the random part of tenant name
|
||||
:param kwargs: Other optional parameters
|
||||
"""
|
||||
self._tenant_create(name_length=name_length, **kwargs)
|
||||
|
||||
while keystone contexts use
|
||||
plugins/openstack/wrappers/keystone.py
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class KeystoneWrapper(object):
|
||||
def __init__(self, client):
|
||||
self.client = client
|
||||
|
||||
def __getattr__(self, attr_name):
|
||||
return getattr(self.client, attr_name)
|
||||
|
||||
@abc.abstractmethod
|
||||
def create_project(self, project_name, domain_name="Default"):
|
||||
"""Creates new project/tenant and return project object.
|
||||
|
||||
:param project_name: Name of project to be created.
|
||||
:param domain_name: Name or id of domain where to create project,
|
||||
for implementations that don't support
|
||||
domains this
|
||||
argument must be None or 'Default'.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def delete_project(self, project_id):
|
||||
"""Deletes project."""
|
||||
|
||||
|
||||
class KeystoneV2Wrapper(KeystoneWrapper):
|
||||
def create_project(self, project_name, domain_name="Default"):
|
||||
self._check_domain(domain_name)
|
||||
tenant = self.client.tenants.create(project_name)
|
||||
return KeystoneV2Wrapper._wrap_v2_tenant(tenant)
|
||||
|
||||
def delete_project(self, project_id):
|
||||
self.client.tenants.delete(project_id)
|
||||
|
||||
class KeystoneV3Wrapper(KeystoneWrapper):
|
||||
def create_project(self, project_name, domain_name="Default"):
|
||||
domain_id = self._get_domain_id(domain_name)
|
||||
project = self.client.projects.create(
|
||||
name=project_name, domain=domain_id)
|
||||
return KeystoneV3Wrapper._wrap_v3_project(project)
|
||||
|
||||
def delete_project(self, project_id):
|
||||
self.client.projects.delete(project_id)
|
||||
|
||||
Users context:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@context.configure(name="users", order=100)
|
||||
class UserGenerator(UserContextMixin, context.Context):
|
||||
"""Context class for generating temporary
|
||||
users/tenants for benchmarks."""
|
||||
|
||||
def _create_tenants(self):
|
||||
cache["client"] = keystone.wrap(clients.keystone())
|
||||
tenant = cache["client"].create_project(
|
||||
self.PATTERN_TENANT % {"task_id": task_id, "iter": i}, domain)
|
||||
|
||||
Suggested change
|
||||
----------------
|
||||
|
||||
.. code-block::
|
||||
|
||||
plugins/
|
||||
|
|
||||
+-- openstack/
|
||||
|
|
||||
|
|
||||
+-- scenarios/
|
||||
| |
|
||||
| |
|
||||
| +-- neutron/
|
||||
| +-- authenticate/
|
||||
|
|
||||
+-- services/
|
||||
| # Here we will store base code for openstack services.
|
||||
| # like wait_for, and wait_for_delete
|
||||
+-- base.py
|
||||
|
|
||||
+-- compute/
|
||||
| |
|
||||
| +-- compute.py
|
||||
|
|
||||
+-- identity/
|
||||
| | # Here is common service when we care to do things
|
||||
| | # and regardless of which API/service is used for
|
||||
| | # that. So we will implement here parts that can be
|
||||
| | # done in both.
|
||||
| +-- identity.py
|
||||
| | # Here is api for working with specific API
|
||||
| | # version/service Like keystone_v2/keystone_v3 or
|
||||
| | # nova_network/neutron. This will be used in
|
||||
| | # main.py for implementation.
|
||||
| +-- kestone_v2.py
|
||||
| |
|
||||
| +-- kestone_v3.py
|
||||
|
|
||||
+-- network/
|
||||
| | # Here is common service when we care to do things
|
||||
| | # and regardless of which API/service is used for
|
||||
| | # that. So we will implement here parts that can be
|
||||
| | # done in both.
|
||||
| +-- network.py
|
||||
| | # Here is api for working with specific API
|
||||
| | # version/service Like nova_network/neutron.
|
||||
| | # This will be used in main.py for implementation.
|
||||
| +-- nova_network.py
|
||||
| |
|
||||
| +-- neutron.py
|
||||
|
|
||||
+-- ...
|
||||
|
||||
|
||||
Base class that allow us to use atomic actions in services is inside the
|
||||
rally/plugins/openstack/services/base.py:
|
||||
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class Service(object):
|
||||
def __init__(self, clients, atomic_inst=None):
|
||||
self.clients = clients
|
||||
if atomic_inst:
|
||||
if not isinstance(atomic_inst, ActionTimerMixin):
|
||||
raise TypeError()
|
||||
|
||||
# NOTE(boris-42): This allows us to use atomic actions
|
||||
# decorators but they will add values
|
||||
# to the scenario or context instance
|
||||
self._atomic_actions = atomic_inst._atomic_actions
|
||||
else:
|
||||
# NOTE(boris-42): If one is using this not for scenarios and
|
||||
# context, Service instance will store atomic
|
||||
# actions data.
|
||||
self._atomic_actions = costilus.OrderedDict()
|
||||
|
||||
|
||||
Implementation of IdentityService in services/identity/identity.py:
|
||||
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class IdentityService(Service):
|
||||
"""Contains only common methods for Keystone V2 and V3."""
|
||||
|
||||
def __init__(self, clients, atomic_inst=None, version=None):
|
||||
super(self).__init__(clients, atomic_inst=atomic_inst)
|
||||
|
||||
if version:
|
||||
if version == "2":
|
||||
self.impl = KeystoneV2Service()
|
||||
else:
|
||||
self.impl = KeysotneServiceV3()
|
||||
else:
|
||||
self.impl = auto_discover_version()
|
||||
|
||||
def project_create(self, name, **kwargs):
|
||||
result = self.impl.project_create(name)
|
||||
# handle the difference between implementations
|
||||
return magic(result)
|
||||
|
||||
# ...
|
||||
|
||||
|
||||
Inside services/identity/keystone_v2.py:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class KeystoneV2Service(KeystoneService):
|
||||
|
||||
# NOTE(boris-42): we can use specific atomic action names
|
||||
# for specific implementation of service
|
||||
@atomic.action_timer("keystone_v2.tenant_create")
|
||||
def project_create(self, project_name):
|
||||
"""Implementation."""
|
||||
|
||||
|
||||
Inside services/identity/keystone_v3.py:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class KeystoneV3Service(KeystoneService):
|
||||
|
||||
@atomic.action_timer("keystone_v3.project_create")
|
||||
def project_create(self, project_name):
|
||||
"""Implementation."""
|
||||
|
||||
def domain_create(self, *args, **kwargs):
|
||||
"""Specific method for KesytoneV3."""
|
||||
|
||||
|
||||
Both context.keystone and scenario.keystone can use now services/identity.py
|
||||
|
||||
usage is the same in context and scenario, so it's enough to show in case
|
||||
of scenario.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from rally.plugins.openstack.services.identity import identity
|
||||
from rally.plugins.openstack.services.identity import keystone_v3
|
||||
|
||||
class KeystoneBasic(scenario.OpenStackScenario): # no more utils.py
|
||||
"""Basic benchmark scenarios for Keystone."""
|
||||
|
||||
|
||||
@validation.number("name_length", minval=10)
|
||||
@validation.required_openstack(admin=True)
|
||||
@scenario.configure(context={"admin_cleanup": ["keystone"]})
|
||||
def create_tenant(self, name_length=10, **kwargs):
|
||||
"""Create a keystone tenant with random name.
|
||||
|
||||
:param name_length: length of the random part of tenant name
|
||||
:param kwargs: Other optional parameters
|
||||
"""
|
||||
|
||||
name = self._generate_random_name(length=name_length)
|
||||
# NOTE(boris-42): Code above works in keystone V2 and V3
|
||||
# as well it will add atomic action, and name
|
||||
# will be "keystone_v3.project_create" or
|
||||
# "keystone_v2.tenant_create" depending on used
|
||||
# version
|
||||
common.Identity(self.clients, self).create_project(name,
|
||||
**kwargs)
|
||||
|
||||
# NOTE(boris-42): If you need specific operation for keystone v3
|
||||
keystone_v3.KeystoneV3Service(self.clients, self).domain_create()
|
||||
|
||||
# NOTE(boris-42): One of the nice thing is that we can move
|
||||
# initialization of services to __init__ method
|
||||
# of sceanrio.
|
||||
|
||||
Assignee(s)
|
||||
-----------
|
||||
|
||||
- boris-42
|
||||
|
||||
Work Items
|
||||
----------
|
||||
|
||||
#. Create a base.Service class
|
||||
#. Create for each project services
|
||||
#. Use in all scenarios and context services instead of utils
|
||||
#. Deprecate utils
|
||||
#. Remove utils
|
||||
|
||||
|
||||
Dependencies
|
||||
============
|
||||
|
||||
none
|
Loading…
Reference in New Issue
Block a user