从 Inspector 迁移检查规则

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

本规范完成了在 /approved/merge-inspector 中启动的工作,即迁移检查规则 API。

请参阅 术语表,了解理解本规范所需的概念。

问题描述

检查是一个相当有主见的流程。 某些领域通常需要特定站点的定制

  • 自动发现。 例如,可以为遵循特定模式的新节点填充凭据,或者从 CMDB 中获取凭据。

  • 验证逻辑。 一些操作员希望如果节点不满足某些条件,则使检查失败。 在自动发现的上下文中,可以使用这种验证来防止意外机器被注册。

这些请求可以通过检查钩子来覆盖。 但是,如 替代方案 中所述,编写和部署钩子可能过于不灵活。

提议的变更

迁移来自 Inspector 的简化版 内省规则

在 Inspector 提供的基础上添加了这些有用的功能

内置规则

允许操作员从 YAML 文件加载规则。 这些规则将始终存在,不会存储在数据库中,也不会被删除。 这些规则既更容易编写钩子,又是 Inspector 笨拙的配置选项(例如 [discovery]enabled_bmc_address_version)的替代品。

阶段

在 Inspector 中,规则始终在处理结束时运行。 我们将添加一个新的字段 phase 到规则中,其值为

  • early - 在任何其他处理之前运行,甚至在自动发现和查找之前。 这样的规则将无法访问节点对象。

  • preprocess - 在所有检查钩子的 preprocess 阶段之后运行,但在主阶段之前运行。

  • main(默认)- 在所有检查钩子之后运行。

更新规则

Inspector 不提供更新规则的 API。 这没有理由,我们将为它们添加 PATCH 支持。

敏感规则

规则的条件和操作可能包含敏感信息,例如 BMC 信息。 如果规则被标记为敏感,则其操作和条件将不会作为 GET 请求响应的一部分返回。 不可能使敏感规则变为不敏感。

运行敏感规则产生的错误消息也将比较简短,以避免意外泄露敏感信息。

优先级

在 Inspector 中,规则始终按照创建顺序运行。 这显然不方便,因此在 Ironic 中,我们将为它们添加优先级。 所有规则都可以使用 0 到 9999 之间的优先级,负值和高于 10000 的值保留给内置规则。 默认优先级为 0。 具有相同优先级的规则仍然按照创建顺序运行,以保持兼容性。

数据库存储

当前,Inspector 将每个规则拆分为三个表(规则、条件和操作)。 这可能从数据库设计角度来看更正确,但实际上使用起来不方便,因为条件和操作永远不会在规则上下文之外访问。 规则也永远不会在没有其条件和操作的情况下被访问。 本规范将它们作为 JSON 字段放在规则表中。

条件和操作的一致参数

条件有一个 field 属性,该属性被特殊处理为节点或清单的字段。 本规范将其更改为具有一个操作和多个参数的结构,请参阅 数据模型影响

备选方案

  • 使用检查钩子机制。 钩子不太灵活,因为它们需要将 Python 代码安装在 Ironic 旁边,并且每次更改服务都需要重新启动。 后者对于基于容器的部署尤其成问题。

  • 彻底更改规则 DSL 为不太笨拙的东西,例如类似 Ansible 的 miniscript。 虽然我最终仍然想这样做,但我认为这项工作会过度增加已经很大的工作范围。 考虑到 API 版本控制,我们始终可以在底层更改语言。

  • 允许 API 用户上传 Python 代码。 没评论。

  • 说真的,用 Lua 或任何其他“成熟”的嵌入式语言编写规则。 我还没有充分研究这个选项。 也许这是前进的方向? 部署者会介意一个新的 C 依赖项(在 liblua 或 LuaJIT 上)吗?

  • 逐字复制检查规则,不删除,不添加。 我认为没有理由为了更好的长期维护进行小的改进。 其中一些添加与安全性相关。

  • 将检查规则变成与 Ironic 分开的服务。 违背了 Inspector 合并的目的。 例如,无法访问节点数据库意味着效率较低的操作。

  • 根本不要迁移检查规则。 它们确实带来了一些复杂性,但也证明了对操作非常有用的工具。 CERN 使用它们,这是我衡量高级操作员有用性的基准。

数据模型影响

改编自 Inspector,并添加了 提议的更改 中描述的补充。

class Rule(Base):
    uuid = Column(String(36), primary_key=True)
    created_at = Column(DateTime, nullable=False)
    updated_at = Column(DateTime, nullable=True)
    priority = Column(Integer, default=0)
    description = Column(String(255), nullable=True)
    scope = Column(String(255), nullable=True)  # indexed
    sensitive = Column(Boolean, default=False)
    phase = Column(String(16), nullable=True)  # indexed
    conditions = Column(db_types.JsonEncodedList(mysql_as_long=True))
    actions = Column(db_types.JsonEncodedList(mysql_as_long=True))

条件和操作

在本规范中,条件和操作都具有相同的基本结构

  • op - 操作:布尔值(条件)或操作(操作)。

  • args - 列表(Python *args 的意义)或字典(Python **kwargs 的意义)的参数。

Inspector 中的操作的特殊属性获得不同的形式

  • 代替 invert:在 op 前面加上一个感叹号(可选空格),例如 eq - !eq

  • 代替仅 multiple,支持类似 Ansible 的 loop 字段。 在操作上,将运行多个操作。 在条件上,multiple 字段定义了如何连接结果。 与 Inspector 相同

    any(默认)

    需要任何匹配

    all

    需要全部匹配

    first

    有效地,在第一次迭代后短路循环

    last

    有效地,仅运行循环的最后一次迭代。

变量插值

字符串参数由 Python 格式化处理,可用对象为 nodeportsport_groupsinventoryplugin_data,例如 {node.driver_info[ipmi_address]}{inventory[interfaces][0][mac_address]}

在早期阶段运行时,仅可用 inventoryplugin_data

node 实际上是一个代理映射,它考虑了 mask_secrets 选项(如 其他部署者影响 中所述)。

如果值是单引号大括号 {} 包围的字符串(没有未格式化的文本),我们将评估内部内容并避免将其转换为字符串。 这样可以将列表和字典传递给操作和 loop。 此行为可能会通过挂接到 Formatter 类来实现。

可用条件

与 Inspector 不同,将构建一个条件列表到 Ironic 中

is-true(value)

检查值是否评估为布尔 True。 除了实际的布尔值外,非零数字和字符串“yes”、“true”(任何大小写)都评估为 True。

is-false(value)

检查值是否评估为布尔 False。 除了实际的布尔值外,零 None 和字符串“no”、“false”(任何大小写)都评估为 False。

注意

对于某些值,这两个条件都可能为 false(例如,随机字符串)。 这是故意的。

is-none(value)

检查值是否为 None。

is-empty(value)

检查值是否为 None 或空字符串、列表或字典。

eq/lt/gt(*values, *, force_strings=False)

检查所有值是否相等/小于/大于。 如果 force_strings,所有值将首先转换为字符串。

注意

Inspector 有 nelege,可以通过 !eq!gt!lt 来实现。

in-net(address, subnet)

检查给定的地址是否在提供的子网中。

contains(value, regex)

检查值是否包含给定的正则表达式。

matches(value, regex)

检查值是否完全匹配给定的正则表达式。

one-of(value, values)

检查值是否在提供的列表中。 类似于 contains,但也适用于非字符串值。 等效于

- op: eq
  args: [<value>, "{item}"]
  loop: <values>

可用操作

与 Inspector 类似,操作将来自入口点 ironic.inspection_rules.actions 的插件。 随 Ironic 一起提供的有

fail(msg)

使用给定的消息使检查失败。

set-plugin-data(path, value)

在插件数据中设置一个值。

extend-plugin-data(path, value, *, unique=False)

将插件数据中的一个值视为一个列表,追加到它。 如果 unique 为 True,则如果该项目存在,则不要追加。

unset-plugin-data(path)

取消设置插件数据中的一个值。

log(msg, level="info")

将消息写入 Ironic 日志。

以下操作在 early 阶段不可用

set-attribute(path, value)

将给定的路径(在 Ironic API 使用的 JSON patch 的意义上)设置为该值。

extend-attribute(path, value, *, unique=False)

将给定的路径视为一个列表,追加到它。

del-attribute(path)

取消设置给定的路径。 在无效的节点属性上失败,但在缺少子字典字段时不失败。

set-port-attribute(port_id, path, value)

设置由 MAC 或 UUID 标识的端口上的值。

extend-port-attribute(port_id, path, value, *, unique=False)

将给定的端口上的路径视为一个列表,追加到它。

del-port-attribute(port_id, path)

取消设置由 MAC 或 UUID 标识的端口上的值。

注意

这里 *path* 是 Ironic API 使用的 JSON patch 的意义上的路径。

示例

部分摘自 Inspector 文档,使用 YAML 格式。

- description: Initialize freshly discovered nodes
  sensitive: true
  conditions:
    - op: is-true
      args: ["{node.auto_discovered}"]
    - op: "!is-empty"
      args: ["{plugin_data[bmc_address}"]
  actions:
    - op: set-attribute
      args: ["/driver", "ipmi"]
    - op: set-attribute
      args: ["/driver_info/ipmi_address", "{plugin_data[bmc_address]}"]
    - op: set-attribute
      args: ["/driver_info/ipmi_username", "admin"]
    - op: set-attribute
      args: ["/driver_info/ipmi_password", "pa$$w0rd"]

注意

plugin_data[bmc_address] 字段是 validate_interfaces 钩子的副作用。

- description: Initialize Dell nodes using IPv6
  sensitive: true
  conditions:
    - op: is-true
      args: ["{node.auto_discovered}"]
    - op: contains
      args: ["{inventory[system_vendor][manufacturer]}", "(?i)dell"]
  actions:
    - op: set-attribute
      args: ["/driver", "idrac"]
    - op: set-attribute
      args: ["/driver_info/redfish_address", "https://{inventory[bmc_v6address]}"]
    - op: set-attribute
      args: ["/driver_info/redfish_username", "root"]
    - op: set-attribute
      args: ["/driver_info/redfish_password", "calvin"]

状态机影响

None(规则正在 INSPECTING 状态下运行)

REST API 影响

迁移 API 大部分逐字,将前缀更改为 inspection_rules,添加 PATCH 和更多列表选项

POST /v1/inspection_rules

创建检查规则。请求体是规则的表示形式。除了 built_in 字段外,所有字段都可以在创建时设置。只有 actions 是必需的(条件为空的规则将无条件运行)。

对于无效输入,返回 HTTP 400 错误。

GET /v1/inspection_rules/<uuid>

返回一个检查规则。输出字段大多重复数据库字段,并添加一个布尔字段 built_in

对于敏感规则,将返回 null 而不是 conditionsactions

如果未找到规则,则返回 HTTP 404 错误。

GET /v1/inspection_rules[?detail=true/false&scope=...&phase=...]

列出所有检查规则。如果 detailfalse 或未指定,则不返回条件和动作。可以按范围和阶段进行过滤。

对于无效输入,返回 HTTP 400 错误。

PATCH /v1/inspection_rules/<uuid>

更新一个规则并返回它。可以更新敏感规则,但结果在任何情况下都不包含条件或动作。

如果未找到规则,则返回 HTTP 404 错误。

如果输入无效,例如尝试修改 built_in,将 sensitive 更改为 false 或将优先级设置为允许范围之外(0 到 9999),则返回 HTTP 400 错误。

DELETE /v1/inspection_rules/<uuid>

删除一个规则。

如果未找到规则,则返回 HTTP 404 错误。

如果规则是内置规则,则返回 HTTP 400 错误。

DELETE /v1/inspection_rules

删除所有规则,但保留内置规则。

客户端 (CLI) 影响

“openstack baremetal” CLI

检查规则的 CRUD 操作,改编自 Introspection Rules CLI,只需将 introspection 替换为 inspection

$ openstack baremetal inspection rule import <file>
$ openstack baremetal inspection rule list [--long]
$ openstack baremetal inspection rule get <rule ID>
$ openstack baremetal inspection rule delete <rule ID>

为了清晰起见,更改了批量删除命令。

$ # Inspector version:
$ openstack baremetal introspection rule purge
$ # New version:
$ openstack baremetal inspection rule delete --all

将支持更新操作。

$ openstack baremetal inspection rule set <rule ID> \
      [--actions '<JSON>'] [--conditions '<JSON>'] \
      [--sensitive] [--scope '<scope>'] [--phase 'early|preprocess|main'] \
      [--uuid '<uuid>'] [--description '<description>']
$ openstack baremetal inspection rule unset <rule ID> \
      [--conditions] [--scope] [--description]

还添加了一种从字段而不是单个 JSON 创建规则的方法。

$ openstack baremetal inspection rule create \
      --actions '<JSON>' [--conditions '<JSON>'] \
      [--sensitive] [--scope '<scope>'] [--phase 'early|preprocess|main'] \
      [--uuid '<uuid>'] [--description '<description>']

“openstacksdk”

baremetal 模块将使用标准的 CRUD 加上批量删除进行更新。

def inspection_rules(details=False): pass
def get_inspection_rule(rule): pass
def patch_inspection_rule(rule, patch): pass
def update_inspection_rule(rule, **fields): pass
def delete_inspection_rule(rule, ignore_missing=True):
def delete_all_inspection_rules(): pass

RPC API 影响

驱动程序 API 影响

不会对驱动程序产生影响。操作员可以选择在具有所有检查接口(包括带外接口)的节点上运行检查规则。

Nova 驱动程序影响

Ramdisk 影响

安全影响

检查规则可以访问所有节点和清单数据。因此,应仅将其限制给管理员。

其他最终用户影响

可扩展性影响

性能影响

拥有大量的检查规则会使检查过程更长。但它不应影响系统的其余部分。

其他部署者影响

新的部分 [inspection_rules] 将具有以下选项

built_in

一个可选的路径,指向包含内置检查规则的 YAML 文件。在服务启动时加载,因此无法通过 SIGHUP 进行修改。

default_scope

对于未设置此字段的所有规则(不包括内置规则),scope 的默认值。

mask_secrets

是否在传递给规则的节点信息中屏蔽密钥。

  • always(默认)- 始终删除像 BMC 密码这样的内容。

  • never - 从不屏蔽任何内容,将完整的节点对象传递给所有规则。

  • sensitive - 允许对标记为 sensitive 的规则使用密钥。

supported_interfaces

一个正则表达式,用于匹配运行检查规则的 inspect 接口。默认值为 ^(agent|inspector)$,以将规则限制为仅限带内实现。可以设置为 .* 以也在所有节点上运行。

将在 [auto_discovery] 部分添加一个选项

inspection_scope

通过自动发现注册的节点的检查范围的默认值。简化了使用检查规则定位此类节点。

开发人员影响

动作通过在 ironic.inspection_rules.actions 命名空间中的入口点处具有插件提供的。

class InspectionRuleActionBase(metaclass=abc.ABCMeta):
    """Abstract base class for rule action plugins."""

    formatted_params = []
    """List of params to be formatted with python format."""

    supports_early = False
    """Whether the action is supported in the early phase."""

    def call_early(self, rule, *args, **kwargs):
        """Run action in the early phase."""
        raise NotImplementedError

    @abc.abstractmethod
    def __call__(self, task, rule, *args, **kwargs):
        """Run action on successful rule match."""

注意

Inspector 中的接口支持几个额外的验证功能。我希望从方法签名中推导出有效的参数。

实现

负责人

主要负责人

Dmitry Tantsur (IRC: dtantsur, dtantsur@protonmail.com)

其他贡献者

待定

工作项

请参阅 RFE。

依赖项

  • /approved/merge-inspector

测试

  • 添加功能测试,以执行检查规则的 CRUD 操作。

  • 更新带内检查作业,使其具有一个简单的规则,我们可以验证它是否正在运行(例如,它在节点的 extra 中设置了一些内容)。

升级和向后兼容性

由于转换可能并不总是简单的(例如,关于变量插值或循环),因此现有的规则将不会自动从 Inspector 迁移到 Ironic。

文档影响

  • 将更新 API 参考。

  • 用户指南将从 Inspector 迁移,并包含一些实际的 示例

参考资料