与 Neutron 同步事件

https://storyboard.openstack.org/#!/story/1304673

大多数 neutron 操作是异步的。我们需要通过利用 neutron 的通知来跟踪它们的结果,neutron 可以在某些事件发生时通过 HTTP 调用将通知发送到 ironic。

问题描述

通过其 API 对 Neutron 资源进行的更新在其后端异步处理。这暴露了与 Ironic 之间潜在的竞争条件。

例如:来自 Ironic 的更新端口 DHCP 设置的 API 请求将在关联的 dnsmasq 配置更新并重启服务器很久之后成功返回。存在一个竞争条件,ironic 可能会在 DHCP 正确配置之前启动机器,尤其是在机器启动速度非常快的情况下(例如,本地 VM)。

另一个问题,具有更严重的安全影响,是如果 Neutron 在配置租户网络时未能绑定端口,ironic 将继续完成部署,将端口绑定到配置网络。

Ironic 应该能够接收来自 neutron 的通知,当端口状态发生变化时。只有与 Ironic 相关的端口才应该导致 neutron 发送通知。本规范侧重于绑定操作,但事件处理程序可以进一步扩展,例如能够处理 DHCP 选项的更新。以下是 neutron 中的 nova notifier 的一个例子 [1]

在本规范中,“通知”一词用于指 neutron 发送的通知。术语“事件”更通用,用于指在 ironic HTTP API 上接收的有效负载。

提议的变更

当 ironic 在配置或清理期间更改 neutron 端口信息(例如,更新端口绑定)或创建端口(多租户集成)时,我们将节点置于 *WAIT 状态,并暂停部署/清理,等待 neutron 通知。由于可以同时更新多个端口,因此我们只需要在发送所有端口的请求之后,才将节点置于 *WAIT 状态。

通知由 neutron 侧生成,并发送到专用的 ironic API 端点 - /events。为此框架已经存在,neutron 侧不需要额外的工作。Nova notifier 可以在 [1] 中看到。外部网络事件将包含以下字段

{
    "events": [
        {
            "event": "network.bind_port"
            "port_id": "VIF UUID",
            "mac_address": "VIF MAC address",
            "status": "VIF port status",
            "device_id": "VIF device ID",
            "binding:host_id": "hostname",
        },
        ...
    ]
}

事件处理程序是一个处理在 /events 端点收到的事件的对象。本规范处理 neutron 通知的情况,但为了使其更通用,建议在相关的驱动程序接口中定义事件处理程序,在接口类的 event_handler 字段中。例如,对于网络相关事件,将如下所示

class NeutronNetwork(common.VIFPortIDMixin,
                     neutron.NeutronNetworkInterfaceMixin,
                     base.NetworkInterface):
    """Neutron v2 network interface"""

    event_handler = NeutronEventHandler()

基础 BaseNetworkEventHandler 类将包含以下方法

class BaseNetworkEventHandler(object):

    @abc.abstractmethod
    def configure_tenant_networks(self, task):
        """Ensures that all tenant ports are ready to be used."""
        pass

    @abc.abstractmethod
    def unconfigure_tenant_networks(self, task):
        """Ensures that all tenant ports are down."""
        pass

    @abc.abstractmethod
    def add_provisioning_network(self, task):
        """Ensures that at least one provisioning port is active."""
        pass

    @abc.abstractmethod
    def remove_provisioning_network(self, task):
        """Ensures that all provisioning ports are deleted."""
        pass

    @abc.abstractmethod
    def add_cleaning_network(self, task):
        """Ensures that at least one cleaning port is active."""
        pass

    @abc.abstractmethod
    def remove_cleaning_network(self, task):
        """Ensures that all cleaning ports are deleted."""
        pass

在处理网络接口的 conductor 方法中(例如,do_node_deploy),我们将把我们期望的事件保存在节点的 driver_internal_info['waiting_for'] 字段中,并调用网络接口方法(这将从部署接口移动到 conductor)。如果调用是同步的,我们将在前一个完成时直接进行到下一个,否则我们将触发状态机“wait”事件,并保存将在相应事件完成时调用的回调。所有回调都将存储在一个简单的字典中,以节点 UUID 为键。 driver_internal_info['waiting_for'] 将是一个简单的字符串列表,其中每个字符串将是相应的驱动程序接口和事件处理程序的方法名称,以便我们知道在异步操作并接收到事件时触发哪个事件处理程序的哪个方法。如果收到意外事件,我们将忽略它(并记录 API 中出现意外事件)。

第三方驱动程序接口方法也可以通过以下方式添加他们想要等待的内容

  • 将事件名称添加到 driver_internal_info['waiting_for'] 列表;

  • 将事件名称添加到全局 per-node CALLBACKS 字典中的回调映射,以及应该用其调用的参数。

这将允许等待在自定义驱动程序接口中注册的自定义事件。

Neutron 不知道应该在请求主体中包含哪个方法名称,因为它只操作 neutron 实体,它了解诸如端口绑定、端口解绑、端口删除等事情。我们将通过简单的字典将我们等待的事情映射到 neutron 传递的事情

NETWORK_HANDLER_TO_EVENT_MAP = {
    'network.unconfigure_tenant_networks': 'network.unbind_port',
    'network.configure_tenant_networks': 'network.bind_port',
    'network.add_provisioning_network': 'network.bind_port',
    'network.remove_provisioning_network': 'network.delete_port',
    'network.add_cleaning_network': 'network.bind_port',
    'network.remove_cleaning_network': 'network.delete_port',
}

当接收到外部网络事件时,如果我们在等待它,ironic API 将执行按 MAC 地址查找节点和按 MAC 地址查找端口,以检查相应的节点和端口是否存在。请求主体中收到的端口状态保存到端口的 internal_info['network_status'],然后触发 process_event。在 conductor 侧,process_event 将通过 NETWORK_HANDLER_TO_EVENT_MAP 将事件名称转换为事件处理程序方法,并调用事件处理程序。Conductor 还会处理状态机转换。

事件处理程序将查看 ironic 资源的 status,例如,对于网络事件,我们希望将 neutron 端口 status 保存到每个端口或端口组的 internal_info['network_status'] 中,并且仅在端口(组)具有所需 status 时才认为异步操作“完成”。在 neutron 生成的事件主体中应该存在需要调用的事件检索上的事件处理程序方法。如果期望的事件是“完成”,则应该将其从 driver_internal_info['waiting_for'] 列表中删除,并且可以通过触发状态机“continue”事件并调用之前保存到 CALLBACKS 字典中的回调来继续部署操作。

为了确保我们永远不会等待事件,通常的 *WAIT 状态超时周期性任务将被使用。将为新的 DELETE WAIT 状态添加一个。可以在 [2] 中看到这样一个周期性任务的例子。

备选方案

  • 使用信号量在等待事件时暂停 greenthread。这将使代码更清晰、更简单,只有一个缺点 - 如果 conductor 被重启,我们将丢失有关我们等待的事件的信息。这仍然比我们现在拥有的更好,并且可能可以解决。这里的另一个缺点是如果同时运行许多 greenthread,以及 conductor 在滚动升级期间停止的事实,可能会出现性能问题。

  • 使用 Neutron 端口 status 轮询。存在一个问题,即使 neutron 端口的 status 是 ACTIVE,某些操作可能尚未完成。neutron 的 notifier 框架为我们处理这个问题。

数据模型影响

无。

状态机影响

引入了一个新的 DELETE WAIT 状态。节点可以从 DELETING 状态移动到它,收到“wait”事件后。当在 DELETE WAIT 状态下触发“continue”事件时,节点将切换回 DELETING 状态。这是引入的原因,因为我们需要在开始清理之前取消配置租户网络。

REST API 影响

需要创建新的端点 POST /events。此端点的默认策略将是 "rule:is_admin"。请求主体格式将如下所示

{
    "events": [
        {
            "event": "network.bind_port"
            "port_id": "VIF UUID",
            "mac_address": "VIF MAC address",
            "status": "VIF port status",
            "device_id": "VIF device ID",
            "binding:host_id": "hostname",
        },
        ...
    ]
}

只有 event 字段是必需的,其格式为 <event_type>.<event>,其中

  • <event_type> 是将被调用的事件处理程序的接口名称,在本规范实现中,只有 network 接口处理程序将被添加。

  • <event> 是发生的事件的名称,它将被转换为当前 <event_type> 接口处理程序的事件处理程序方法名称,该方法将被调用。

如果期望的事件处理失败,conductor 将触发 fail 状态机事件。

此端点上的请求的正常响应代码是 200 (OK),错误代码是

  • 400 (Bad Request),如果没有任何事件处理程序可以处理该事件。

  • 404 (Not Found),如果使用旧的 API 微版本标头发出请求,或者如果无法通过请求主体中发送的 MAC 地址找到节点。

  • 401 (Unauthorized),如果提供的凭据的授权被拒绝。

  • 403 (Forbidden),如果发出请求的用户无权使用此端点。

客户端 (CLI) 影响

客户端将被更新以支持发送外部通知。此功能仅将添加到客户端的 python API 中,不会引入新的命令。新方法只是将收到的 JSON 传递到 /events 端点。

此方法将由 neutron 中的 ironic notifier 模块用于将通知发送到 ironic API。

RPC API 影响

将添加一个新的方法 process_event。在此处处理收到的外部事件。

在 conductor 侧的此方法中,我们将比较当前我们存储在 driver_internal_info['waiting_for'] 字段中等待的事件与事件主体中的接收到的 "event"。如果我们为所有端口(组)收到了期望的事件,我们将触发状态机上的 continue 事件,并且之前保存到 per-node CALLBACKS 字典中的回调将被调用。

驱动程序 API 影响

作为此更改的一部分,为了确保网络接口调用发生,并且我们等待它们的完成,我们需要使 add_{cleaning,provisioning}_network 网络接口方法幂等,以便我们可以在 conductor 中调用它们而不会破坏树外网络接口。

Nova 驱动程序影响

无。

安全影响

随着网络接口调用移动到 conductor,并等待它们的成功完成,我们确保网络配置与我们期望的一致,从而增强安全性,并消除端口仍然映射到配置网络而 remove_provisioning_networkconfigure_tenant_networks 方法异步失败时的错误。

其他最终用户影响

需要配置 neutron notifier。它需要 keystone 管理员凭据以及(如果未提供,将从 keystone 端点目录中发现)将事件发送到的 ironic API 地址。

可扩展性影响

无。

性能影响

节点配置和取消配置可能需要一些额外的时间,当我们等待外部事件时。

其他部署者影响

无。

Ramdisk 影响

无。

开发人员影响

开发人员将能够为他们希望在配置期间使用的任何事件创建所需的事件处理程序,并将这些事件处理程序添加到驱动程序接口。

实现

负责人

主要负责人

vdrok

其他贡献者

工作项

  1. 将事件处理程序添加到 neutron 和 flat 网络接口。

  2. 添加 process_event conductor 方法,该方法将处理事件。

  3. 添加 /events 端点。

  4. 实现客户端方面的更改。

依赖项

  • 在 InstanceDeployFailure 的情况下,Nova 应该只清理属于“compute”的端口 [3]

测试

将提供集成和单元测试。

升级和向后兼容性

这不会影响正常的升级过程。要使用事件,需要升级 API 和 conductor。在升级期间,需要在 neutron 中配置 ironic notifier。无需启用此功能,它将默认启用。

在滚动升级的情况下,首先升级 ironic conductors,然后升级 ironic APIs,然后重新配置 neutron 以启用 notifier。

如果我们决定使所有网络接口调用异步,则启用 neutron notifier 的步骤将成为强制性的,否则操作员将必须手动将通知发送到 ironic API,或者部署将因未收到网络事件而超时。这部分可能需要在审查期间重新审视:)

文档影响

此功能将在开发人员文档和 API 参考中进行记录。

参考资料