从 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)吗?

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

  • 将检查规则变成与 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 补丁的意义上)设置为该值。

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 补丁的意义上的路径。

示例

部分摘自 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"]

状态机影响

无(规则正在 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。

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

DELETE /v1/inspection_rules/<uuid>

删除一条规则。

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

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

DELETE /v1/inspection_rules

删除所有规则,除了内置规则。

客户端 (CLI) 影响

“openstack baremetal” CLI

检查规则 CRUD,改编自 内省规则 CLI,只需将 内省 替换为 检查

$ 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

一个正则表达式,用于匹配运行检查规则的 检查接口。默认值为 ^(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 迁移,并包含一些实际的 示例

参考资料