API 演进:etag ID

随着时间的推移,API 接口已经演进,社区也涌现出更多的用例和用户。这导致识别出当前 REST API 的一些不足之处:它不如人们期望的用户友好,并且它无法服务于我们希望它服务的全部用例。

本规范描述了在 API 响应中实现 etag 标识符,从而更好地解决并发 API 客户端之间的冲突。

https://bugs.launchpad.net/ironic/+bug/1605728

问题描述

多个客户端尝试更新相同的资源时,可能会在不知情的情况下意外地覆盖彼此的更改。这被称为“丢失更新”问题,这是裸机服务中的一个问题。通常通过使用“etag”标识符来解决“丢失更新”问题,如本 API 工作组 关于 etag 的规范 中所述。本规范建议裸机服务提供 etag 支持以解决此问题。

提议的变更

裸机服务将开始在内部存储每个 API 可修改资源(Node、Port、Portgroup、Chassis)的唯一 etag 标识符。此标识符将在对请求的资源进行 GET 请求时,在新微版本引入后,在响应体和标头中返回。主要有两种情况

  • GET 单个资源(子资源)。ETag 将在响应标头和主体中返回。Python 客户端可以使用新的 etag 操作资源(参见客户端部分)。Ironic shell 用户在响应中看到 etag。

  • GET 资源列表(子资源)。每个资源的 Etag 将在响应主体中作为资源字段提供

    {
    ‘ports’: [
    {

    uuid: ‘11111111-2222-3333-4444-555555555555’,

    <所有其他字段>,

    etag: ‘W/eeeeeeeeeeeeee’

    },

    {

    uuid: ‘66666666-7777-8888-9999-222222222222’,

    <所有其他字段>,

    etag: ‘W/tttttttttttttt’

    }

    ]

    }

    请注意,将 etag 放入标头没有意义,因为在客户端以标准和简单的方式区分 etag 是不可能的。

所有修改资源或子资源的请求,无论是通过 PUT、PATCH 还是 DELETE 请求,都应该开始要求在请求中提供带有适当 etag 标识符的 If-Match 标头(对于子资源,它是该子资源的 etag)。请注意,根据 RFC 标准 If-Match 标头规范,If-Match 不是必需的。

因此,此处使用了 SHOULD 关键字,这意味着 If-Match 标头将是客户端的可选参数(参见 指示需求级别的关键字)。

重要的是,根据 rfc 使用 entity-tags If-Match 必须由客户端提供才能成功通过请求。当 ETags 用于缓存验证时,这很有用。具体到我们的用例,强制使用 If-Match 标头会降低 ironic 客户端的易用性。用户有权决定他们是否想了解他们所做的更改。

If-None-Match 或任何其他“预条件标头字段”将不受支持。

为了节省请求效率,If-Match 标头应在 API 和 conductor 端通过将提供的 If-Match 标头与当前的 etag 字符串进行比较来验证。如果提供的标头与实际值不匹配,则应返回 412 Precondition Failed

在成功接收对资源或子资源的任何修改请求时,服务器应生成一个新的 etag 标识符(它不得依赖于请求中包含的 ironic-api-version)。如果修改是同步的,并且响应已经包含资源的表示形式,则新的 etag 标识符应包含在响应主体和标头中。

Etag 是从紧凑编码的 JSON 字符串(字典的顺序无关紧要)生成的 SHA-512 哈希,该字符串来自 oslo 版本化对象字段的字典。Etag 将为所有创建和修改请求生成,并考虑到除被忽略的字段之外的字段

对于节点,忽略的字段是

driver_internal_info, etag, updated_at

对于端口、端口组和机箱,它仅是 etagupdated_at

生成的 etag 将包含“W/”前缀,以指定使用了弱验证器。强验证器更适合于比较,但在我们的用例中,考虑到 etag 并非每次更新都会更改,并且在元数据更改时也不会更改(例如,Content-Type),则应用弱验证器。参见 弱与强验证器

备选方案

如果我们不实现 etag 支持,我们将没有任何手段来防止客户端的 PATCH 请求之间的竞争,并且我们将无法实现使用 PUT 作为更新资源或子资源的一种方式。

数据模型影响

由于在每个 REST API 请求中,对象都从数据库获取,并且只要它没有被更改,etag 应该从数据库返回的对象中检索。这比一遍又一遍地花费时间生成哈希更有效。

为此,应在每个资源表(Node、Port、Portgroup、Chassis)中添加一个内部 etag 字段来存储 etag 标识符。Etag 标识符将是字符串字段,数据长度限制为 130 个字符(etag 是一个包含前缀“W/”的字符串 + 128 位长的 SHA-512 十六进制数字)。

还应将新的字段 etag 添加到对象模型中,以与数据库层保持一致。对象层也将是根据当前对象字段重新生成 etag 的位置。

Etag 也将包含在通知有效负载中,使其更灵活和可用。

状态机影响

REST API 影响

从特定的 API 微版本开始,所有 GET 和 POST 请求以及同步 PATCH 和 PUT 请求的响应中都必须发送新的 etag 标头。

相同的 If-Match etag 标头应在所有 PUT、PATCH 和 DELETE 请求中接受。这意味着每个提供任何更新功能的端点都应该具有验证 etag(可选)的逻辑。

应引入一个新的错误状态 412 PreconditionFailed,并用于向客户端发出信号,表明他们的资源版本已过时,当提供的 etag 标头与服务器的版本不匹配时。

客户端 (CLI) 影响

使用新的微版本,客户端可以有能力在了解他们所做的更改的情况下更新资源。为此,他们应该在修改任何资源的请求的标头中发送一个带有 etag 标识符的 If-Match 标头。有两种选择:通过 CLI 或通过 Python Client API 执行此操作。后一种选择适用于云中使用的任何 python 开发人员脚本(对于生产很有用)。

ironicclient shell 中 etag 用法的流程

  • 客户端发送 GET 请求。

  • 从特定的 API 微版本开始,响应应在标头中包含请求的资源(资源)的 etag。ETag 也应包含在返回的资源主体中,无论是获取单个资源还是资源集合。

  • 如果需要,用户可以通过在命令中添加 --etag 标志来指定 Etag。Etag 可以从响应的主体或标头中获取

    ironic --ironic-api-version 1.40 node-update \
    --etag <etag_string> driver_info/foo=value
    

    此 etag 字符串作为 If-Match 标头发送到请求。

  • Ironic API 弹出 If-Match 标头,将其与 rpc_node 的 etag 进行检查,如果它们匹配,则实体标签将进一步发送到 RPC,conductor 在那里再次验证它。如果 etag 在某个点不匹配,将引发 412 PreconditionFailed 错误。如果请求的 X-Openstack-Ironic-Api-Version 不支持 etag,则引发 NotAcceptable 错误。

为了使 Python Client API 可用而无需 shell,资源将存储为功能齐全的对象(而不仅仅是属性包),包括 etag 标识符。为此,ironicclient API 将被重写,以便 Resource 类能够更新自身并调用管理器以发送 NodeManager 中可用的请求。 Resource 将像所有 Python 对象一样在进程执行期间存储在内存中。

对于 Python API,Resource 对象的任何适当操作都将接受可选的 etag 参数。流程如下

  • 在 Python Shell 或某些脚本中,客户端向资源发送 GET 请求。响应中返回的 etag 将存储在资源表示形式中。例如:

    node = node_manager.get(node_ident)

  • 之后,用户脚本可以随时对资源本身执行操作

    new_node = node.update(patch, etag=True)

    之后,如果服务器验证了请求,他们将拥有最新的资源表示形式。

    请注意,对于 1 个标准的弃用周期,If-Match 默认情况下不会发送到服务器。客户端将被警告,在下一个版本中,etag 参数将默认设置为 True

如果请求 etag,它将从当前资源表示形式中检索

(如 node.etaggetattr(node, 'etag'))。之后,它将作为 If-Match 标头发送,这意味着用户关心最新的信息。如果资源中不存在 etag,并且客户端没有关闭 etag 选项,则如果使用大于或等于 etag API 版本的 API,他们将失败。

根据情况,客户端可以选择透明地重试,或者向用户显示存储版本与服务器端资源之间的差异。客户端还应开始提醒用户,如果由于资源冲突而更新请求失败。

“ironic” CLI

见上文。

“openstack baremetal” CLI

与客户端影响中描述的相同流程。

RPC API 影响

RPC API 版本需要升级才能接受资源操作的 etag 参数。 etag 参数,默认为 None,应传递给适当的方法。

驱动程序 API 影响

Nova 驱动程序影响

Nova ironic 驱动程序可以使用新的 Ironic API 微版本,因此 ironic api 版本用于 nova virt 驱动程序需要升级。在 python ironicclient API 中 etag 选项为 True 之前,在 nova 驱动程序中,我们应该通过 Node 资源对象显式指定 etag=False

Ramdisk 影响

安全影响

其他最终用户影响

发送 If-Match 标头可能会因 412 Precondition Failed 错误而失败。客户端可以尝试使用新的 etag 或/并且显示两个资源表示形式之间的差异。

可扩展性影响

性能影响

新的 etag 生成可能会根据资源大小增加响应时间。

其他部署者影响

一些服务(例如 Nova)通过 API 更改裸机资源,因此他们可以升级 Ironic API 以使用 etag。如果服务不升级,请警告部署者,跳过这些升级可能会违反一些强烈建议,并且 ironic 侧的信息一致性无法保证。

开发人员影响

Python 开发人员可以将 Resource 对象作为功能齐全的对象来使用,并对它们执行修改操作。他们还可以实现使用 etag 选项并行高效地使用的脚本。

实现

负责人

主要负责人

galyna

其他贡献者

vdrok

工作项

  • 实施数据库迁移,将内部 etag 字段添加到所有顶级资源。

  • 在通用代码中实施生成和验证实用程序函数。

  • 在 ironic.api.controllers.v1 模块中实施更改,以便在获取或更改资源时接受和返回 etag 标识符。

  • 实施单元测试和 tempest 测试。

  • 更新 api-ref 文档。

  • 在 python 客户端库和 openstack CLI 中实施更改,以开始在 GET 请求上缓存 etag,并在 PUT/PATCH/DELETE 请求上发送 etag。

依赖项

测试

应添加单元和 tempest 测试,以确保返回 etag 标识符,它们通过修改资源的请求得到验证,并且当提供无效(或仅仅不是当前)etag 时,会返回适当的错误。

升级和向后兼容性

保留了向后兼容性,因为 etag 仅应在新微版本中返回和需要。

此更改不包括对任何资源的实质性更改。

文档影响

应在我们的 API 参考中记录 etag 标识符的正确使用方法。

参考资料