rally/doc/specs/in-progress/refactor_scenario_utils.rst
sunyandi 0b3297ab61 modify keystone spelling errors
Change-Id: I2824b7df5d14a9f08b21777d0fb6a5333753220a
2018-02-06 16:15:18 +08:00

11 KiB

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

rally/
|
+-- plugins/
    +-- openstack/
    |   +-- scenarios/
    |   |   |
    |   |   +-- nova/
    |   |   |   |
    |   |   |   +-- servers.py
    |   |   |   |
    |   |   |   +-- utils.py
    |   |   |
    |   |   +-- ...
    |   +-- wrappers/
    |       |
    |       +-- keystone.py
    |       |
    |       +-- network.py

keystone scenarios use plugins/openstack/scenarios/keystone/utils.py

@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)
class KeystoneBasic(kutils.KeystoneScenario):
    """Basic 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

@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:

@context.configure(name="users", order=100)
class UserGenerator(UserContextMixin, context.Context):
    """Context class for generating temporary users/tenants for testing."""

    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

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.
         |   +-- keystone_v2.py
         |   |
         |   +-- keystone_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:

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:

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 = KeystoneServiceV3()
        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:

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:

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 KeystoneV3."""

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.

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 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 scenario.

Assignee(s)

  • boris-42

Work Items

  1. Create a base.Service class
  2. Create for each project services
  3. Use in all scenarios and context services instead of utils
  4. Deprecate utils
  5. Remove utils

Dependencies

none