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
对于端口、端口组和机箱,它仅是 etag 和 updated_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.etag 或 getattr(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 标识符的正确使用方法。