searchusermenu
  • 发布文章
  • 消息中心
点赞
收藏
评论
分享
原创

Rally自动化测试框架详解之Cleanup

2023-09-26 23:58:01
31
0

1. Context

代码:rally-openstack/rally_openstack/contexts/cleanup/user.py

@validation.add(name="check_cleanup_resources", admin_required=False)
# NOTE(amaretskiy): Set maximum order to run this last
@context.configure(name="cleanup", platform="openstack", order=sys.maxsize - 1,
                   hidden=True)
class UserCleanup(base.CleanupMixin, context.Context):
    """Context class for user resources cleanup."""

    def cleanup(self):
        manager.cleanup(
            names=self.config,
            admin_required=False,
            users=self.context.get("users", []),
            api_versions=self.context["config"].get("api_versions"),
            superclass=scenario.OpenStackScenario,
            task_id=self.get_owner_id()
        )

代码:rally-openstack/rally_openstack/contexts/cleanup/admin.py

@validation.add(name="check_cleanup_resources", admin_required=True)
# NOTE(amaretskiy): Set order to run this just before UserCleanup
@context.configure(name="admin_cleanup", platform="openstack",
                   order=(sys.maxsize - 2), hidden=True)
class AdminCleanup(base.CleanupMixin, context.Context):
    """Context class for admin resources cleanup."""

    def cleanup(self):
        manager.cleanup(
            names=self.config,
            admin_required=True,
            admin=self.context["admin"],
            users=self.context.get("users", []),
            api_versions=self.context["config"].get("api_versions"),
            superclass=scenario.OpenStackScenario,
            task_id=self.get_owner_id())

代码:rally-openstack/rally_openstack/contexts/cleanup/base.py

class CleanupMixin(object):

    CONFIG_SCHEMA = {
        "type": "array",
        "$schema": consts.JSON_SCHEMA,
        "items": {
            "type": "string",
        }
    }

    def setup(self):
        pass

上述这些代码,可以看出:

  • context实现了cleanup方法(setup方法也实现了,但什么也没做)
  • rally的cleanup分为cleanupadmin_cleanup,区别在于:cleaup只清理admin_required==False的资源,admin_cleanup只清理admin_required==True的资源;cleanup的order更大,因此比admin_cleanup先执行;
  • 具体清理是调用cleanup.manager.cleanup


2. Cleaup Manager

2.1 cleaup方法

代码:rally-openstack/rally_openstack/cleanup/manager.py

def cleanup(names=None, admin_required=None, admin=None, users=None,
            api_versions=None, superclass=plugin.Plugin, task_id=None):
    """Generic cleaner.

    This method goes through all plugins. Filter those and left only plugins
    with _service from services or _resource from resources.

    Then goes through all passed users and using cleaners cleans all related
    resources.

    :param names: Use only resource managers that have names in this list.
                  There are in as _service or
                  (%s.%s % (_service, _resource)) from
    :param admin_required: If None -> return all plugins
                           If True -> return only admin plugins
                           If False -> return only non admin plugins
    :param admin: rally.deployment.credential.Credential that corresponds to
                  OpenStack admin.
    :param users: List of OpenStack users that was used during testing.
                  Every user has next structure:
                  {
                    "id": <uuid1>,
                    "tenant_id": <uuid2>,
                    "credential": <rally.deployment.credential.Credential>
                  }
    :param superclass: The plugin superclass to perform cleanup
                       for. E.g., this could be
                       ``rally.task.scenario.Scenario`` to cleanup all
                       Scenario resources.
    :param task_id: The UUID of task
    """
    resource_classes = [cls for cls in discover.itersubclasses(superclass)
                        if issubclass(cls, rutils.RandomNameGeneratorMixin)]
    if not resource_classes and issubclass(superclass,
                                           rutils.RandomNameGeneratorMixin):
        resource_classes.append(superclass)
    for manager in find_resource_managers(names, admin_required):
        LOG.debug("Cleaning up %(service)s %(resource)s objects"
                  % {"service": manager._service,
                     "resource": manager._resource})
        SeekAndDestroy(manager, admin, users,
                       api_versions=api_versions,
                       resource_classes=resource_classes,
                       task_id=task_id).exterminate()

上述代码展示了cleanup的过程:

  • 查找OpenStackScenario的子类,该类须是rutils.RandomNameGeneratorMixin的子类,一般这些类定义在rally-openstack/rally_openstack/scenarios目录里,这些类的run方法定义了测试过程
  • 查找ResourceManager的子类,这些类定义在rally-openstack/rally_openstack/cleanup/resources.py文件中,这些类用来定义某一类资源的具体删除方法等,查找过程见下面的说明
  • 根据查找到的资源控制类,实例化SeekAndDestroy通过其exterminate方法来清理资源

 2.2 find_resource_managers方法

代码:rally-openstack/rally_openstack/cleanup/manager.py

def find_resource_managers(names=None, admin_required=None):
    """Returns resource managers.

    :param names: List of names in format <service> or <service>.<resource>
                  that is used for filtering resource manager classes
    :param admin_required: None -> returns all ResourceManagers
                           True -> returns only admin ResourceManagers
                           False -> returns only non admin ResourceManagers
    """
    names = set(names or [])

    resource_managers = []
    for manager in discover.itersubclasses(base.ResourceManager):
        if admin_required is not None:
            if admin_required != manager._admin_required:
                continue

        if (manager._service in names
           or "%s.%s" % (manager._service, manager._resource) in names):
            resource_managers.append(manager)

    resource_managers.sort(key=lambda x: x._order)

    found_names = set()
    for mgr in resource_managers:
        found_names.add(mgr._service)
        found_names.add("%s.%s" % (mgr._service, mgr._resource))

    missing = names - found_names
    if missing:
        LOG.warning("Missing resource managers: %s" % ", ".join(missing))

    return resource_managers

上述代码展示了资源控制类的查找过程:

  • 查找ResourceManager的子类,这些类定义在rally-openstack/rally_openstack/cleanup/resources.py文件中
  • 根据参数admin_required过滤子类,_admin_required属性在资源控制类定义时,通过装饰器赋值,例如
@base.resource("cinder", "volume_types", order=next(_cinder_order),
               admin_required=True, perform_for_admin_only=True)
class CinderVolumeType(base.ResourceManager):
    pass
  • 根据上下文配置参数中的资源名称,继续过滤子类,_service_resource属性在资源控制类定义时,通过装饰器赋值,例如上面的代码,"cinder"为_service属性,"volume_types"为_resource属性
  • 按照_order属性对资源进行排序,即order越小越先清理,_order属性的赋值方法同上

 ps:下面是装饰器base.resource的定义:

代码:rally-openstack/rally_openstack/cleanup/base.py

def resource(service, resource, order=0, admin_required=False,
             perform_for_admin_only=False, tenant_resource=False,
             max_attempts=3, timeout=CONF.openstack.resource_deletion_timeout,
             interval=1, threads=CONF.openstack.cleanup_threads):
    """Decorator that overrides resource specification.

    Just put it on top of your resource class and specify arguments that you
    need.

    :param service: It is equal to client name for corresponding service.
                    E.g. "nova", "cinder" or "zaqar"
    :param resource: Client manager name for resource. E.g. in case of
                     nova.servers you should write here "servers"
    :param order: Used to adjust priority of cleanup for different resource
                  types
    :param admin_required: Admin user is required
    :param perform_for_admin_only: Perform cleanup for admin user only
    :param tenant_resource: Perform deletion only 1 time per tenant
    :param max_attempts: Max amount of attempts to delete single resource
    :param timeout: Max duration of deletion in seconds
    :param interval: Resource status pooling interval
    :param threads: Amount of threads (workers) that are deleting resources
                    simultaneously
    """

    def inner(cls):
        # TODO(boris-42): This can be written better I believe =)
        cls._service = service
        cls._resource = resource
        cls._order = order
        cls._admin_required = admin_required
        cls._perform_for_admin_only = perform_for_admin_only
        cls._max_attempts = max_attempts
        cls._timeout = timeout
        cls._interval = interval
        cls._threads = threads
        cls._tenant_resource = tenant_resource

        return cls

    return inner

2.3 SeekAndDestroy

该类定义了如何查找创建的资源及删除的方法,通过生产者/消费者模型进行构建

代码:rally-openstack/rally_openstack/cleanup/manager.py

class SeekAndDestroy(object):

    def __init__(self, manager_cls, admin, users, api_versions=None,
                 resource_classes=None, task_id=None):
        """Resource deletion class.

        This class contains method exterminate() that finds and deletes
        all resources created by Rally.

        :param manager_cls: subclass of base.ResourceManager
        :param admin: admin credential like in context["admin"]
        :param users: users credentials like in context["users"]
        :param api_versions: dict of client API versions
        :param resource_classes: Resource classes to match resource names
                                 against
        :param task_id: The UUID of task to match resource names against
        """
        self.manager_cls = manager_cls
        self.admin = admin
        self.users = users or []
        self.api_versions = api_versions
        self.resource_classes = resource_classes or [
            rutils.RandomNameGeneratorMixin]
        self.task_id = task_id

    def _get_cached_client(self, user):
        """Simplifies initialization and caching OpenStack clients."""
        if not user:
            return None
        # NOTE(astudenov): Credential now supports caching by default
        return user["credential"].clients(api_info=self.api_versions)

    def _delete_single_resource(self, resource):
        """Safe resource deletion with retries and timeouts.

        Send request to delete resource, in case of failures repeat it few
        times. After that pull status of resource until it's deleted.

        Writes in LOG warning with UUID of resource that wasn't deleted

        :param resource: instance of resource manager initiated with resource
                         that should be deleted.
        """

        msg_kw = {
            "uuid": resource.id(),
            "name": resource.name() or "",
            "service": resource._service,
            "resource": resource._resource
        }

        LOG.debug(
            "Deleting %(service)s.%(resource)s object %(name)s (%(uuid)s)"
            % msg_kw)

        try:
            rutils.retry(resource._max_attempts, resource.delete)
        except Exception as e:
            msg = ("Resource deletion failed, max retries exceeded for "
                   "%(service)s.%(resource)s: %(uuid)s.") % msg_kw

            if logging.is_debug():
                LOG.exception(msg)
            else:
                LOG.warning("%(msg)s Reason: %(e)s" % {"msg": msg, "e": e})
        else:
            started = time.time()
            failures_count = 0
            while time.time() - started < resource._timeout:
                try:
                    if resource.is_deleted():
                        return
                except Exception as e:
                    LOG.exception(
                        "Seems like %s.%s.is_deleted(self) method is broken "
                        "It shouldn't raise any exceptions."
                        % (resource.__module__, type(resource).__name__))

                    # NOTE(boris-42): Avoid LOG spamming in case of bad
                    #                 is_deleted() method
                    failures_count += 1
                    if failures_count > resource._max_attempts:
                        break

                finally:
                    rutils.interruptable_sleep(resource._interval)

            LOG.warning("Resource deletion failed, timeout occurred for "
                        "%(service)s.%(resource)s: %(uuid)s." % msg_kw)

    def _publisher(self, queue):
        """Publisher for deletion jobs.

        This method iterates over all users, lists all resources
        (using manager_cls) and puts jobs for deletion.

        Every deletion job contains tuple with two values: user and resource
        uuid that should be deleted.

        In case of tenant based resource, uuids are fetched only from one user
        per tenant.
        """
        def _publish(admin, user, manager):
            try:
                for raw_resource in rutils.retry(3, manager.list):
                    queue.append((admin, user, raw_resource))
            except Exception:
                LOG.exception(
                    "Seems like %s.%s.list(self) method is broken. "
                    "It shouldn't raise any exceptions."
                    % (manager.__module__, type(manager).__name__))

        if self.admin and (not self.users
                           or self.manager_cls._perform_for_admin_only):
            manager = self.manager_cls(
                admin=self._get_cached_client(self.admin))
            _publish(self.admin, None, manager)

        else:
            visited_tenants = set()
            admin_client = self._get_cached_client(self.admin)
            for user in self.users:
                if (self.manager_cls._tenant_resource
                   and user["tenant_id"] in visited_tenants):
                    continue

                visited_tenants.add(user["tenant_id"])
                manager = self.manager_cls(
                    admin=admin_client,
                    user=self._get_cached_client(user),
                    tenant_uuid=user["tenant_id"])
                _publish(self.admin, user, manager)

    def _consumer(self, cache, args):
        """Method that consumes single deletion job."""
        admin, user, raw_resource = args

        manager = self.manager_cls(
            resource=raw_resource,
            admin=self._get_cached_client(admin),
            user=self._get_cached_client(user),
            tenant_uuid=user and user["tenant_id"])

        if (isinstance(manager.name(), base.NoName) or
                rutils.name_matches_object(
                    manager.name(), *self.resource_classes,
                    task_id=self.task_id, exact=False)):
            self._delete_single_resource(manager)

    def exterminate(self):
        """Delete all resources for passed users, admin and resource_mgr."""

        broker.run(self._publisher, self._consumer,
                   consumers_count=self.manager_cls._threads)

方法_publisher定义了如何查找资源:

  • 第115行和第128行:实例化某一个具体的资源控制类,比如servers
  • 第105,106行:使用资源控制类的list方法,来列出各个用户创建的资源,ps: list方法一般是调用各个组件的list方法
  • 将列出的资源存入队列

方法_consumer定义了如何删除资源:

  • 第138-142行:实例化某一具体的资源控制类(从队列中取出资源,并赋值);
  • 第144-147行:根据资源的命名规则过滤资源的名称; ps:或者名称是NoName的实例
  • 调用资源控制类的delete方法来删除资源,一般是调用各个组件的delete方法,同时会检查是否删除成功

 

3. ResourceManager

rally cleanup是通过这些ResourceManager类来发现和清理资源的,对于某一类资源需要定义一个ResourceManager来对应,比如 nova.servers,这些类定义在rally-openstack/rally_openstack/cleanup/resources.py文件中

3.1 基类ResourceManager

代码:rally-openstack/rally_openstack/cleanup/base.py

@resource(service=None, resource=None)
class ResourceManager(object):
    """Base class for cleanup plugins for specific resources.

    You should use @resource decorator to specify major configuration of
    resource manager. Usually you should specify: service, resource and order.

    If project python client is very specific, you can override delete(),
    list() and is_deleted() methods to make them fit to your case.
    """

    def __init__(self, resource=None, admin=None, user=None, tenant_uuid=None):
        self.admin = admin
        self.user = user
        self.raw_resource = resource
        self.tenant_uuid = tenant_uuid

    def _manager(self):
        client = self._admin_required and self.admin or self.user
        return getattr(getattr(client, self._service)(), self._resource)

    def id(self):
        """Returns id of resource."""
        return self.raw_resource.id

    def name(self):
        """Returns name of resource."""
        return self.raw_resource.name

    def is_deleted(self):
        """Checks if the resource is deleted.

        Fetch resource by id from service and check it status.
        In case of NotFound or status is DELETED or DELETE_COMPLETE returns
        True, otherwise False.
        """
        try:
            resource = self._manager().get(self.id())
        except Exception as e:
            return getattr(e, "code", getattr(e, "http_status", 400)) == 404

        return utils.get_status(resource) in ("DELETED", "DELETE_COMPLETE")

    def delete(self):
        """Delete resource that corresponds to instance of this class."""
        self._manager().delete(self.id())

    def list(self):
        """List all resources specific for admin or user."""
        return self._manager().list()

资源控制类需要继承base.ResourceManager,另外需要注意以下几点:

  • 需要使用装饰器resource来设置属性,比如serviceresourceorderadmin_required等,这些属性前面已经说明
  • 根据具体资源的情况,来重写方法listdeleteis_deleted,list用来查找资源,delete用来删除资源,is_deleted用来判断资源是否成功删除

3.2 资源删除顺序

资源在删除时,需要注意顺序,我们以neutron组件来看一下各个资源的删除

从代码中我们可以看出,neutron服务的删除顺序为:

bgpvpn ---> floatingip ---> port ---> subnet ---> network ---> router ---> security_group ---> quota

另外在删除资源时,需要注意与其他资源的关联情况,当删除subnet,network,router时,需要注意先remove port资源,下面看一下port资源的删除

代码:rally-openstack/rally_openstack/cleanup/resources.py

@base.resource("neutron", "port", order=next(_neutron_order),
               tenant_resource=True)
class NeutronPort(NeutronMixin):
    # NOTE(andreykurilin): port is the kind of resource that can be created
    #   automatically. In this case it doesn't have name field which matches
    #   our resource name templates.
    ROUTER_INTERFACE_OWNERS = ("network:router_interface",
                               "network:router_interface_distributed",
                               "network:ha_router_replicated_interface")

    ROUTER_GATEWAY_OWNER = "network:router_gateway"

    def __init__(self, *args, **kwargs):
        super(NeutronPort, self).__init__(*args, **kwargs)
        self._cache = {}

    def _get_resources(self, resource):
        if resource not in self._cache:
            resources = getattr(self._manager(), "list_%s" % resource)()
            self._cache[resource] = [r for r in resources[resource]
                                     if r["tenant_id"] == self.tenant_uuid]
        return self._cache[resource]

    def list(self):
        ports = self._get_resources("ports")
        for port in ports:
            if not port.get("name"):
                parent_name = None
                if (port["device_owner"] in self.ROUTER_INTERFACE_OWNERS or
                        port["device_owner"] == self.ROUTER_GATEWAY_OWNER):
                    # first case is a port created while adding an interface to
                    #   the subnet
                    # second case is a port created while adding gateway for
                    #   the network
                    port_router = [r for r in self._get_resources("routers")
                                   if r["id"] == port["device_id"]]
                    if port_router:
                        parent_name = port_router[0]["name"]
                if parent_name:
                    port["parent_name"] = parent_name
        return ports

    def name(self):
        return self.raw_resource.get("parent_name",
                                     self.raw_resource.get("name", ""))

    def delete(self):
        device_owner = self.raw_resource["device_owner"]
        if (device_owner in self.ROUTER_INTERFACE_OWNERS or
                device_owner == self.ROUTER_GATEWAY_OWNER):
            if device_owner == self.ROUTER_GATEWAY_OWNER:
                self._manager().remove_gateway_router(
                    self.raw_resource["device_id"])

            self._manager().remove_interface_router(
                self.raw_resource["device_id"], {"port_id": self.id()})
        else:
            from neutronclient.common import exceptions as neutron_exceptions

            try:
                self._manager().delete_port(self.id())
            except neutron_exceptions.PortNotFoundClient:
                # Port can be already auto-deleted, skip silently
                LOG.debug("Port %s was not deleted. Skip silently because "
                          "port can be already auto-deleted." % self.id())

另外,也需要注意各个服务之间的删除顺序,cleanup的删除顺序为:

nova ---> neutron ---> cinder ---> glance ---> keystone

0条评论
0 / 1000
刘****磊
5文章数
0粉丝数
刘****磊
5 文章 | 0 粉丝
原创

Rally自动化测试框架详解之Cleanup

2023-09-26 23:58:01
31
0

1. Context

代码:rally-openstack/rally_openstack/contexts/cleanup/user.py

@validation.add(name="check_cleanup_resources", admin_required=False)
# NOTE(amaretskiy): Set maximum order to run this last
@context.configure(name="cleanup", platform="openstack", order=sys.maxsize - 1,
                   hidden=True)
class UserCleanup(base.CleanupMixin, context.Context):
    """Context class for user resources cleanup."""

    def cleanup(self):
        manager.cleanup(
            names=self.config,
            admin_required=False,
            users=self.context.get("users", []),
            api_versions=self.context["config"].get("api_versions"),
            superclass=scenario.OpenStackScenario,
            task_id=self.get_owner_id()
        )

代码:rally-openstack/rally_openstack/contexts/cleanup/admin.py

@validation.add(name="check_cleanup_resources", admin_required=True)
# NOTE(amaretskiy): Set order to run this just before UserCleanup
@context.configure(name="admin_cleanup", platform="openstack",
                   order=(sys.maxsize - 2), hidden=True)
class AdminCleanup(base.CleanupMixin, context.Context):
    """Context class for admin resources cleanup."""

    def cleanup(self):
        manager.cleanup(
            names=self.config,
            admin_required=True,
            admin=self.context["admin"],
            users=self.context.get("users", []),
            api_versions=self.context["config"].get("api_versions"),
            superclass=scenario.OpenStackScenario,
            task_id=self.get_owner_id())

代码:rally-openstack/rally_openstack/contexts/cleanup/base.py

class CleanupMixin(object):

    CONFIG_SCHEMA = {
        "type": "array",
        "$schema": consts.JSON_SCHEMA,
        "items": {
            "type": "string",
        }
    }

    def setup(self):
        pass

上述这些代码,可以看出:

  • context实现了cleanup方法(setup方法也实现了,但什么也没做)
  • rally的cleanup分为cleanupadmin_cleanup,区别在于:cleaup只清理admin_required==False的资源,admin_cleanup只清理admin_required==True的资源;cleanup的order更大,因此比admin_cleanup先执行;
  • 具体清理是调用cleanup.manager.cleanup


2. Cleaup Manager

2.1 cleaup方法

代码:rally-openstack/rally_openstack/cleanup/manager.py

def cleanup(names=None, admin_required=None, admin=None, users=None,
            api_versions=None, superclass=plugin.Plugin, task_id=None):
    """Generic cleaner.

    This method goes through all plugins. Filter those and left only plugins
    with _service from services or _resource from resources.

    Then goes through all passed users and using cleaners cleans all related
    resources.

    :param names: Use only resource managers that have names in this list.
                  There are in as _service or
                  (%s.%s % (_service, _resource)) from
    :param admin_required: If None -> return all plugins
                           If True -> return only admin plugins
                           If False -> return only non admin plugins
    :param admin: rally.deployment.credential.Credential that corresponds to
                  OpenStack admin.
    :param users: List of OpenStack users that was used during testing.
                  Every user has next structure:
                  {
                    "id": <uuid1>,
                    "tenant_id": <uuid2>,
                    "credential": <rally.deployment.credential.Credential>
                  }
    :param superclass: The plugin superclass to perform cleanup
                       for. E.g., this could be
                       ``rally.task.scenario.Scenario`` to cleanup all
                       Scenario resources.
    :param task_id: The UUID of task
    """
    resource_classes = [cls for cls in discover.itersubclasses(superclass)
                        if issubclass(cls, rutils.RandomNameGeneratorMixin)]
    if not resource_classes and issubclass(superclass,
                                           rutils.RandomNameGeneratorMixin):
        resource_classes.append(superclass)
    for manager in find_resource_managers(names, admin_required):
        LOG.debug("Cleaning up %(service)s %(resource)s objects"
                  % {"service": manager._service,
                     "resource": manager._resource})
        SeekAndDestroy(manager, admin, users,
                       api_versions=api_versions,
                       resource_classes=resource_classes,
                       task_id=task_id).exterminate()

上述代码展示了cleanup的过程:

  • 查找OpenStackScenario的子类,该类须是rutils.RandomNameGeneratorMixin的子类,一般这些类定义在rally-openstack/rally_openstack/scenarios目录里,这些类的run方法定义了测试过程
  • 查找ResourceManager的子类,这些类定义在rally-openstack/rally_openstack/cleanup/resources.py文件中,这些类用来定义某一类资源的具体删除方法等,查找过程见下面的说明
  • 根据查找到的资源控制类,实例化SeekAndDestroy通过其exterminate方法来清理资源

 2.2 find_resource_managers方法

代码:rally-openstack/rally_openstack/cleanup/manager.py

def find_resource_managers(names=None, admin_required=None):
    """Returns resource managers.

    :param names: List of names in format <service> or <service>.<resource>
                  that is used for filtering resource manager classes
    :param admin_required: None -> returns all ResourceManagers
                           True -> returns only admin ResourceManagers
                           False -> returns only non admin ResourceManagers
    """
    names = set(names or [])

    resource_managers = []
    for manager in discover.itersubclasses(base.ResourceManager):
        if admin_required is not None:
            if admin_required != manager._admin_required:
                continue

        if (manager._service in names
           or "%s.%s" % (manager._service, manager._resource) in names):
            resource_managers.append(manager)

    resource_managers.sort(key=lambda x: x._order)

    found_names = set()
    for mgr in resource_managers:
        found_names.add(mgr._service)
        found_names.add("%s.%s" % (mgr._service, mgr._resource))

    missing = names - found_names
    if missing:
        LOG.warning("Missing resource managers: %s" % ", ".join(missing))

    return resource_managers

上述代码展示了资源控制类的查找过程:

  • 查找ResourceManager的子类,这些类定义在rally-openstack/rally_openstack/cleanup/resources.py文件中
  • 根据参数admin_required过滤子类,_admin_required属性在资源控制类定义时,通过装饰器赋值,例如
@base.resource("cinder", "volume_types", order=next(_cinder_order),
               admin_required=True, perform_for_admin_only=True)
class CinderVolumeType(base.ResourceManager):
    pass
  • 根据上下文配置参数中的资源名称,继续过滤子类,_service_resource属性在资源控制类定义时,通过装饰器赋值,例如上面的代码,"cinder"为_service属性,"volume_types"为_resource属性
  • 按照_order属性对资源进行排序,即order越小越先清理,_order属性的赋值方法同上

 ps:下面是装饰器base.resource的定义:

代码:rally-openstack/rally_openstack/cleanup/base.py

def resource(service, resource, order=0, admin_required=False,
             perform_for_admin_only=False, tenant_resource=False,
             max_attempts=3, timeout=CONF.openstack.resource_deletion_timeout,
             interval=1, threads=CONF.openstack.cleanup_threads):
    """Decorator that overrides resource specification.

    Just put it on top of your resource class and specify arguments that you
    need.

    :param service: It is equal to client name for corresponding service.
                    E.g. "nova", "cinder" or "zaqar"
    :param resource: Client manager name for resource. E.g. in case of
                     nova.servers you should write here "servers"
    :param order: Used to adjust priority of cleanup for different resource
                  types
    :param admin_required: Admin user is required
    :param perform_for_admin_only: Perform cleanup for admin user only
    :param tenant_resource: Perform deletion only 1 time per tenant
    :param max_attempts: Max amount of attempts to delete single resource
    :param timeout: Max duration of deletion in seconds
    :param interval: Resource status pooling interval
    :param threads: Amount of threads (workers) that are deleting resources
                    simultaneously
    """

    def inner(cls):
        # TODO(boris-42): This can be written better I believe =)
        cls._service = service
        cls._resource = resource
        cls._order = order
        cls._admin_required = admin_required
        cls._perform_for_admin_only = perform_for_admin_only
        cls._max_attempts = max_attempts
        cls._timeout = timeout
        cls._interval = interval
        cls._threads = threads
        cls._tenant_resource = tenant_resource

        return cls

    return inner

2.3 SeekAndDestroy

该类定义了如何查找创建的资源及删除的方法,通过生产者/消费者模型进行构建

代码:rally-openstack/rally_openstack/cleanup/manager.py

class SeekAndDestroy(object):

    def __init__(self, manager_cls, admin, users, api_versions=None,
                 resource_classes=None, task_id=None):
        """Resource deletion class.

        This class contains method exterminate() that finds and deletes
        all resources created by Rally.

        :param manager_cls: subclass of base.ResourceManager
        :param admin: admin credential like in context["admin"]
        :param users: users credentials like in context["users"]
        :param api_versions: dict of client API versions
        :param resource_classes: Resource classes to match resource names
                                 against
        :param task_id: The UUID of task to match resource names against
        """
        self.manager_cls = manager_cls
        self.admin = admin
        self.users = users or []
        self.api_versions = api_versions
        self.resource_classes = resource_classes or [
            rutils.RandomNameGeneratorMixin]
        self.task_id = task_id

    def _get_cached_client(self, user):
        """Simplifies initialization and caching OpenStack clients."""
        if not user:
            return None
        # NOTE(astudenov): Credential now supports caching by default
        return user["credential"].clients(api_info=self.api_versions)

    def _delete_single_resource(self, resource):
        """Safe resource deletion with retries and timeouts.

        Send request to delete resource, in case of failures repeat it few
        times. After that pull status of resource until it's deleted.

        Writes in LOG warning with UUID of resource that wasn't deleted

        :param resource: instance of resource manager initiated with resource
                         that should be deleted.
        """

        msg_kw = {
            "uuid": resource.id(),
            "name": resource.name() or "",
            "service": resource._service,
            "resource": resource._resource
        }

        LOG.debug(
            "Deleting %(service)s.%(resource)s object %(name)s (%(uuid)s)"
            % msg_kw)

        try:
            rutils.retry(resource._max_attempts, resource.delete)
        except Exception as e:
            msg = ("Resource deletion failed, max retries exceeded for "
                   "%(service)s.%(resource)s: %(uuid)s.") % msg_kw

            if logging.is_debug():
                LOG.exception(msg)
            else:
                LOG.warning("%(msg)s Reason: %(e)s" % {"msg": msg, "e": e})
        else:
            started = time.time()
            failures_count = 0
            while time.time() - started < resource._timeout:
                try:
                    if resource.is_deleted():
                        return
                except Exception as e:
                    LOG.exception(
                        "Seems like %s.%s.is_deleted(self) method is broken "
                        "It shouldn't raise any exceptions."
                        % (resource.__module__, type(resource).__name__))

                    # NOTE(boris-42): Avoid LOG spamming in case of bad
                    #                 is_deleted() method
                    failures_count += 1
                    if failures_count > resource._max_attempts:
                        break

                finally:
                    rutils.interruptable_sleep(resource._interval)

            LOG.warning("Resource deletion failed, timeout occurred for "
                        "%(service)s.%(resource)s: %(uuid)s." % msg_kw)

    def _publisher(self, queue):
        """Publisher for deletion jobs.

        This method iterates over all users, lists all resources
        (using manager_cls) and puts jobs for deletion.

        Every deletion job contains tuple with two values: user and resource
        uuid that should be deleted.

        In case of tenant based resource, uuids are fetched only from one user
        per tenant.
        """
        def _publish(admin, user, manager):
            try:
                for raw_resource in rutils.retry(3, manager.list):
                    queue.append((admin, user, raw_resource))
            except Exception:
                LOG.exception(
                    "Seems like %s.%s.list(self) method is broken. "
                    "It shouldn't raise any exceptions."
                    % (manager.__module__, type(manager).__name__))

        if self.admin and (not self.users
                           or self.manager_cls._perform_for_admin_only):
            manager = self.manager_cls(
                admin=self._get_cached_client(self.admin))
            _publish(self.admin, None, manager)

        else:
            visited_tenants = set()
            admin_client = self._get_cached_client(self.admin)
            for user in self.users:
                if (self.manager_cls._tenant_resource
                   and user["tenant_id"] in visited_tenants):
                    continue

                visited_tenants.add(user["tenant_id"])
                manager = self.manager_cls(
                    admin=admin_client,
                    user=self._get_cached_client(user),
                    tenant_uuid=user["tenant_id"])
                _publish(self.admin, user, manager)

    def _consumer(self, cache, args):
        """Method that consumes single deletion job."""
        admin, user, raw_resource = args

        manager = self.manager_cls(
            resource=raw_resource,
            admin=self._get_cached_client(admin),
            user=self._get_cached_client(user),
            tenant_uuid=user and user["tenant_id"])

        if (isinstance(manager.name(), base.NoName) or
                rutils.name_matches_object(
                    manager.name(), *self.resource_classes,
                    task_id=self.task_id, exact=False)):
            self._delete_single_resource(manager)

    def exterminate(self):
        """Delete all resources for passed users, admin and resource_mgr."""

        broker.run(self._publisher, self._consumer,
                   consumers_count=self.manager_cls._threads)

方法_publisher定义了如何查找资源:

  • 第115行和第128行:实例化某一个具体的资源控制类,比如servers
  • 第105,106行:使用资源控制类的list方法,来列出各个用户创建的资源,ps: list方法一般是调用各个组件的list方法
  • 将列出的资源存入队列

方法_consumer定义了如何删除资源:

  • 第138-142行:实例化某一具体的资源控制类(从队列中取出资源,并赋值);
  • 第144-147行:根据资源的命名规则过滤资源的名称; ps:或者名称是NoName的实例
  • 调用资源控制类的delete方法来删除资源,一般是调用各个组件的delete方法,同时会检查是否删除成功

 

3. ResourceManager

rally cleanup是通过这些ResourceManager类来发现和清理资源的,对于某一类资源需要定义一个ResourceManager来对应,比如 nova.servers,这些类定义在rally-openstack/rally_openstack/cleanup/resources.py文件中

3.1 基类ResourceManager

代码:rally-openstack/rally_openstack/cleanup/base.py

@resource(service=None, resource=None)
class ResourceManager(object):
    """Base class for cleanup plugins for specific resources.

    You should use @resource decorator to specify major configuration of
    resource manager. Usually you should specify: service, resource and order.

    If project python client is very specific, you can override delete(),
    list() and is_deleted() methods to make them fit to your case.
    """

    def __init__(self, resource=None, admin=None, user=None, tenant_uuid=None):
        self.admin = admin
        self.user = user
        self.raw_resource = resource
        self.tenant_uuid = tenant_uuid

    def _manager(self):
        client = self._admin_required and self.admin or self.user
        return getattr(getattr(client, self._service)(), self._resource)

    def id(self):
        """Returns id of resource."""
        return self.raw_resource.id

    def name(self):
        """Returns name of resource."""
        return self.raw_resource.name

    def is_deleted(self):
        """Checks if the resource is deleted.

        Fetch resource by id from service and check it status.
        In case of NotFound or status is DELETED or DELETE_COMPLETE returns
        True, otherwise False.
        """
        try:
            resource = self._manager().get(self.id())
        except Exception as e:
            return getattr(e, "code", getattr(e, "http_status", 400)) == 404

        return utils.get_status(resource) in ("DELETED", "DELETE_COMPLETE")

    def delete(self):
        """Delete resource that corresponds to instance of this class."""
        self._manager().delete(self.id())

    def list(self):
        """List all resources specific for admin or user."""
        return self._manager().list()

资源控制类需要继承base.ResourceManager,另外需要注意以下几点:

  • 需要使用装饰器resource来设置属性,比如serviceresourceorderadmin_required等,这些属性前面已经说明
  • 根据具体资源的情况,来重写方法listdeleteis_deleted,list用来查找资源,delete用来删除资源,is_deleted用来判断资源是否成功删除

3.2 资源删除顺序

资源在删除时,需要注意顺序,我们以neutron组件来看一下各个资源的删除

从代码中我们可以看出,neutron服务的删除顺序为:

bgpvpn ---> floatingip ---> port ---> subnet ---> network ---> router ---> security_group ---> quota

另外在删除资源时,需要注意与其他资源的关联情况,当删除subnet,network,router时,需要注意先remove port资源,下面看一下port资源的删除

代码:rally-openstack/rally_openstack/cleanup/resources.py

@base.resource("neutron", "port", order=next(_neutron_order),
               tenant_resource=True)
class NeutronPort(NeutronMixin):
    # NOTE(andreykurilin): port is the kind of resource that can be created
    #   automatically. In this case it doesn't have name field which matches
    #   our resource name templates.
    ROUTER_INTERFACE_OWNERS = ("network:router_interface",
                               "network:router_interface_distributed",
                               "network:ha_router_replicated_interface")

    ROUTER_GATEWAY_OWNER = "network:router_gateway"

    def __init__(self, *args, **kwargs):
        super(NeutronPort, self).__init__(*args, **kwargs)
        self._cache = {}

    def _get_resources(self, resource):
        if resource not in self._cache:
            resources = getattr(self._manager(), "list_%s" % resource)()
            self._cache[resource] = [r for r in resources[resource]
                                     if r["tenant_id"] == self.tenant_uuid]
        return self._cache[resource]

    def list(self):
        ports = self._get_resources("ports")
        for port in ports:
            if not port.get("name"):
                parent_name = None
                if (port["device_owner"] in self.ROUTER_INTERFACE_OWNERS or
                        port["device_owner"] == self.ROUTER_GATEWAY_OWNER):
                    # first case is a port created while adding an interface to
                    #   the subnet
                    # second case is a port created while adding gateway for
                    #   the network
                    port_router = [r for r in self._get_resources("routers")
                                   if r["id"] == port["device_id"]]
                    if port_router:
                        parent_name = port_router[0]["name"]
                if parent_name:
                    port["parent_name"] = parent_name
        return ports

    def name(self):
        return self.raw_resource.get("parent_name",
                                     self.raw_resource.get("name", ""))

    def delete(self):
        device_owner = self.raw_resource["device_owner"]
        if (device_owner in self.ROUTER_INTERFACE_OWNERS or
                device_owner == self.ROUTER_GATEWAY_OWNER):
            if device_owner == self.ROUTER_GATEWAY_OWNER:
                self._manager().remove_gateway_router(
                    self.raw_resource["device_id"])

            self._manager().remove_interface_router(
                self.raw_resource["device_id"], {"port_id": self.id()})
        else:
            from neutronclient.common import exceptions as neutron_exceptions

            try:
                self._manager().delete_port(self.id())
            except neutron_exceptions.PortNotFoundClient:
                # Port can be already auto-deleted, skip silently
                LOG.debug("Port %s was not deleted. Skip silently because "
                          "port can be already auto-deleted." % self.id())

另外,也需要注意各个服务之间的删除顺序,cleanup的删除顺序为:

nova ---> neutron ---> cinder ---> glance ---> keystone

文章来自个人专栏
文章 | 订阅
0条评论
0 / 1000
请输入你的评论
0
0