本作品采用知识共享署名 3.0 非移植许可协议授权。 http://creativecommons.org/licenses/by/3.0/legalcode

Hook Points API

Hook Point API 蓝图

Designate 从设计上来说,需要与遗留 DNS 应用程序进行接口。组织通常会有其他系统需要与 DNS 交互,反之亦然。Hook Points API 提供了一种通用的方法,可以添加组织特定的代码,在默认使用情况下不会产生任何开销,并且在代码挂接到方法或函数调用时不会允许更改底层 API。

问题描述

目前,为了支持组织特定的功能,需要

  1. 维护 Designate 代码库的分支

  2. 通过模块对代码进行猴子补丁

  3. 将 Designate 视为黑盒,并使用 HTTP 级别的工具添加组织特定的代码。

虽然维护分支可能是一种合理的解决方案,但如果进行大量更改,则很难合并上游更改。猴子补丁需要对代码库有相同的了解,但由于导入更改,即使是小的代码更改也可能悄无声息地失败。最后,将 Designate 视为黑盒通常需要组织特定的代码来重新实现 Designate 的某些方面,因为粒度级别有限。

Hook Points API 提供了一种实用的受支持的方式来注入代码,同时避免上述陷阱。

提议的变更

Hook Points API 提供了一个装饰器,用于将函数或方法定义为 Hook Point。

@hookpoints.hook_point('pool_manager_create_domain')
def create_domain(self, context, domain):
    ...

在上面的例子中,Hook Point 是一个**命名的** Hook Point,名为 pool_manager_create_domain。如果没有提供名称,Hook Point 将使用模块和方法的路径。

@hookpoints.hook_point()
def create_domain(self, context, domain):
     ...

名称是函数模块和名称的组合。

'%s.%s' % (func.__module__, func.__name__)

如果未定义 Hook Point,则原始函数将按原样返回。

为了定义 Hook Point,必须安装一个提供 designate.hook_point 入口点的包。

from setuptools import setup


setup(
    name='raxdns',
    entry_points: {
        'designate.hook_point': [
            'pool_manager_create_domain = raxdns.hooks.pool_manager:create_domain'
        ]
    }
)

安装包后,Hook Point 就会**激活**,这意味着当目标被调用时,它将被调用。如果需要,可以通过配置禁用它。

Hook Point 实现

由于 Hook Point 将作为装饰器应用,因此它是一个实现 __call__ 方法的对象,该方法接受一个函数作为参数,并返回一个适当的函数作为结果。实现者需要确保 Hook Point 正确实现 Hook 目标的 API。

为了方便起见,有一个 BaseHook 可以用来重用常见的模式。

class BaseHook(object):

    OPTS = [
        cfg.BoolOpt('disabled', default=False)
    ]

    def __init__(self, group):
        self.group = group

    @property
    def disabled(self):
        return cfg.CONF[self.group].get('disabled', False)

    def wrapper(self, *args, **kw):
        return self.hook_target(*args, **kw)

    def __call__(self, f):
        # Save our hook target as an attribute for our wrapper method
        self.hook_target = f

        @functools.wraps(self.hook_target)
        def wrapper(*args, **kw):
            if self.disabled:
                return self.hook_target(*args, **kw)
            return self.hook(*args, **kw)
        return wrapper

BaseHook 会处理

  1. 正确使用 functools.wrap

  2. 在配置为禁用时禁用 Hook

  3. 设置配置组,以便以后使用配置

  4. 简化装饰器实现

这个基类旨在简化 Hook 的开发。Hook 作者也可以将 Hook 实现为普通的装饰器。

配置

重要的是要注意,任何配置都必须通过 oslo.config 访问。原因是 Hook 在导入时应用,而配置通常在运行时加载。因此,Hook 在 Hook 目标实际被调用之前可能无法访问配置数据。

Hook 示例

这是一个 Hook Point 的示例,它包装了 Pool Manager 服务中的 create_domain 方法。它验证域名是否不存在于另一个也可以通过相同的后端管理域名的应用程序中。

import requests
from oslo_log import log as logging
from oslo_config import cfg

from designate.pool_manager.service import ERROR_STATUS
from designate.hookpoints import BaseHook


LOG = logging.getLogger(__name__)


class CheckDCXDomainHook(BaseHook):
    OPTS = BaseHook.OPTS + [
        cfg.Opt('legacy_dns', required=True),
    ]

    @property
    def sess(self):
        if not hasattr(self, '_sess'):
            sess = requests.Session()
            self._sess = sess
        return self._sess

    @property
    def legacy_dns(self):
        return cfg.CONF[self.group].legacy_dns

    def hook(self, obj, context, domain):
        resp = self.sess.get(self.legacy_dns + '/find_domains?name=%s' % domain.name)

        # The domain is not found in the legacy system. Let Designate create it
        if not resp.ok:
            # We got a 404, so let Designate make the call
            return self.hook_target(obj, context, domain)

        # The legacy system owns the domain. Notify central it was
        # an error.
        #
        # The `obj` is the Service object.
        obj.central_api.update_status(
            context, domain.id, ERROR_STATUS, domain.serial
        )

Hook Point 作者有责任成为一个好的公民,并正确处理原始代码中的任何错误/返回值,以及支持任何内部 API。

再次强调,Hook Point API 的目的是允许组织注入代码的一种方式,这意味着对代码有相当深入的了解。

Hook Point 管理和配置

Hook Point 通过安装包含 designate.hook_point 入口点的包来安装。默认情况下,这些将被**启用**,并在调用特定的 Hook Point 目标时被调用。这些 Hook **可能**在配置中以 Hook Point 级别被**禁用**。

[hook_point:pool_manager_create_domain]
disabled = True

如果需要,Hook Point 还可以接收配置详细信息。

[hook_point:pool_manager_create_domain]
legacy_dns_api = https://my.dns.legacy.org.net:8975

配置将通过全局 oslo.config.cfg.CONF 对象提供。

Central 变更

Storage 变更

其他变更

Hook Point 可以自由地添加,或者在极其有限的已知用例中添加。同样,也可以不正式地添加 Hook Point,并且组织可以根据需要将其应用于组织特定的补丁。

替代方案

除了最初提到的钩入 Designate 代码的策略之外,还可以针对每个用例创建更具体的 Hook Point。例如,可以为从队列读取消息时调用的 Hook 创建一个非常具体的 API。虽然提供更具体的 Hook 可能会允许针对特定用例的 API,但它还需要为每个 API 进行设计、文档记录和测试。Hook Point API 提供了一种经过测试的注入代码的单一方式,这种方式对 API 的影响有限,并允许合理的支持级别。

实现

负责人

主要负责人

eric-larson

里程碑

完成目标里程碑

Liberty-1

工作项

  • 添加 designate.hookpoints,实现 @hook_point 装饰器

  • 添加编写 Hook Point 的文档并枚举现有的 Hook Point。

请参阅 此评审 以获取当前实现。

依赖项

  • stevedore