驱动程序组合改革

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

本规范建议改进我们命名和组合驱动程序的方式,使其从接口出发。这将允许一个厂商驱动程序具有可按节点配置的选项,而不是为每个厂商提供许多驱动程序。

问题描述

我们的驱动程序接口矩阵变得越来越复杂。更糟糕的是,现在我们有很多接口可以用于每个驱动程序。例如

  • boot:大多数驱动程序支持 PXE 和 iPXE,而有些还支持虚拟介质;支持petitboot引导加载程序已被提议。

  • deploy:支持两种部署方法:通过 iSCSI 写入镜像或从代理内部直接写入。

  • inspect:有使用 ironic-inspector 的通用检查,但有些驱动程序也允许带外检查。此功能是可选的,因此我们应该提供一种禁用它的方法。

目前,我们最终得到一个复杂且非常令人困惑的命名方案。例如

  • pxe_ipmitool 使用 PXE 或 iPXE 进行引导,并使用 iSCSI 进行部署。

  • agent_ipmitool 实际上也使用 PXE 或 iPXE,但它不使用 iSCSI。

  • 更糟糕的是,pxe_ipmitool 实际上正在使用代理!

  • 为了反映所有可能性,名称可能需要更像 pxe_iscsi_ipmitoolipxe_iscsi_ipmitoolpxe_direct_ipmitoolipxe_direct_ipmitool 等。

  • 现在,对我们拥有的每个电源驱动程序重复相同的操作。

提议的变更

简介

本规范中使用以下概念

vendor

驱动程序的驱动力。它可以是硬件厂商或 ironic 团队本身,对于通用驱动程序(如 IPMI)而言。这还包括树外驱动程序。

硬件接口(或简称 接口

替换术语“驱动程序接口”的概念 - 一组处理裸机配置某些方面的厂商特定功能。例如,现在我们有 powerdeployinspectboot 和其他一些接口。

硬件类型

从 ironic 的角度来看,支持相同接口集合的硬件系列。这可以像支持 IPMI 协议的所有硬件一样宽泛,也可以像支持某些特定接口的几个硬件型号一样窄泛。

driver

一个包含指向硬件接口链接的精简对象。在本次规范之前,driver 的含义大致与本次规范中的 hardware type 的含义相同。

经典驱动程序

本次规范之前的 ironic 驱动程序:一个类,其中接口链接硬编码在 Python 代码中。

动态驱动程序

一个在运行时创建的driver,其链接到接口是根据节点记录中的信息(包括硬件类型和接口)生成的。

通过本规范,我们将实现以下目标

  • 厂商负责定义一组优先级顺序排列的支持的接口实现。

  • 允许厂商保证不受支持的接口实现不会与他们定义的硬件类型一起使用。这是通过硬件类型列出它支持的所有接口来实现的。

  • 允许第三方创建树外硬件类型,从而最大限度地重用树内接口实现。

  • 使硬件类型定义尽可能地声明式。

  • 允许用户像在本次规范之前更改驱动程序一样,切换节点的硬件类型

  • 允许用户通过裸机 API 切换由硬件类型支持的节点的接口实现。

配置

  • 硬件类型定义为 Python 类 - 参见 硬件类型 以获取详细信息。创建了一个入口点,为每个硬件类型提供一个简单的名称,例如

    ironic.hardware.types =
        generic-ipmi = ironic.hardware.ipmi:GenericIpmiHardware
        ilo-gen8 = ironic.hardware.ilo:iLOGen8Hardware
        ilo-gen9 = ironic.hardware.ilo:iLOGen9Hardware
    
  • 硬件接口列表仍然硬编码在 Python 代码中,不能通过插件扩展。接口的实现方式与本次规范之前相同:通过继承 ironic.drivers.base 中的适当抽象类。

  • 对于每个硬件接口,所有实现都获得自己的入口点和一个唯一的名称,例如

    ironic.hardware.interfaces.power =
        ipmitool = ironic.drivers.modules.ipmitool:IpmitoolPower
    
  • 硬件类型硬件接口实现之间的兼容性在 Python 代码中表示 - 参见 硬件类型 以获取详细信息。

  • 创建一个新的配置选项 enabled_hardware_types,其中包含一个启用的硬件类型列表。这不包括通过现有的 enabled_drivers 选项启用的经典驱动程序

  • 创建一个配置选项族 default_<INTERFACE>_interface,允许操作员显式设置在创建新节点时默认接口,如果创建请求中未指定接口。

    此处 <INTERFACE> 是接口类型:power、management 等。

  • 创建一个配置选项族 enabled_<INTERFACE>_interfaces,其中包含一个启用的每个硬件接口的实现列表,可在 ironic 部署中使用。

    如果 default_<INTERFACE>_interface 提供了默认值,则该值必须在此列表中,否则 conductor 将无法启动。

  • 如果用户在节点创建请求中未显式请求接口实现,则使用以下方式计算的值

    • 如果设置了 default_<INTERFACE>_interface,则使用其值。如果它不受节点硬件类型的支持,则返回错误。

    • 否则,从部署配置中定义的 enabled_<INTERFACE>_interfaces硬件类型的优先级排序的 supported_<INTERFACE>_interfaces 列表的交集中选择第一个可用的接口实现。如果此交集为空,则返回错误。

    计算出的默认值将在创建时存储在节点的数据库条目中。

  • 更改我们加载驱动程序的方式,而不是单个驱动程序的单例实例,我们将为每个节点提供一个动态驱动程序实例,其中包含指向硬件接口实现的链接(就像今天一样)。

    但是,接口实现本身仍然是单例,将在启动期间预加载并存储在 conductor 中。

    如果任何启用的硬件类型接口实现无法加载(例如,由于缺少依赖项),则 conductor 将无法启动。

    注意

    虽然从技术上讲,可以启用未在任何启用的硬件类型中使用的接口,但在此情况下不会加载它们。

    经典驱动程序将以与之前相同的方式加载。

  • 修改周期性任务收集代码,以也收集每个启用硬件类型的启用接口的周期性任务。

  • 如果经典驱动程序硬件类型之间存在名称冲突,则 conductor 将无法启动。

数据库和 REST API

  • 允许节点 driver 字段接受硬件类型。这将在所有 API 版本中起作用。

    注意

    原因有两个

    • 一致性:我们从未阻止使用旧 API 版本的新驱动程序,并且动态驱动程序对用户来说看起来很像新驱动程序。

    • 可用性:我们计划最终弃用经典驱动程序。删除它们时,所有客户端都需要在注册节点时指定硬件类型。为了允许旧客户端继续与 API 服务交互,即使它们使用新的驱动程序名称(硬件类型),我们必须继续使用相同的字段名称和 API 语义。

  • 对于每个接口,在 node 表上创建一个名为 <interface_name>_interface 的新字段。每次添加新接口时都需要进行迁移(希望不会太频繁)。

    对于硬件类型,将 <interface_name>_interface 字段设置为 None 表示使用如 配置 中所述计算出的默认值。

    尝试将这些字段中的任何一个设置为除 None 之外的值,如果 driver 字段设置为经典驱动程序,则将导致错误。同样,如果 driver 字段设置为经典驱动程序,则所有这些字段都将重置为 None

  • 每次更新 driver 和/或任何接口字段时,conductor 都会检查硬件类型是否支持由此产生的接口(除非 driver 设置为经典驱动程序)。

    要切换到两个不兼容的接口集合,所有更改应在一个 API 调用中完成。例如,对于具有 ilo-gen8 硬件类型vmedia_ilo 引导接口的节点,以下 JSON 补丁将被允许

    [
        {"op": "replace", "path": "/boot_interface", "value": "ipxe"},
        {"op": "replace", "path": "/driver", "value": "generic-ipmi"}
    ]
    

    但是,以下补丁将失败,因为引导接口不兼容

    [
        {"op": "replace", "path": "/driver", "value": "generic-ipmi"},
    ]
    

    注意

    RFC 6902 要求 JSON 补丁是原子的,因为 HTTP PATCH 操作必须是原子的。这意味着,只要最终结果一致,就可能导致对象不一致。

    验证将在 API 服务端进行,通过检查新的 conductor_hardware_interfaces 数据库表。

  • 如果由于某种原因,现有的接口对节点无效(例如,在节点注册后禁用了它),则将通过通常的节点验证 API 报告。此接口的验证将无法通过,并显示适当的错误消息。在编程级别,该接口的驱动程序属性(例如 task.driver.deploy)将被设置为 None

  • 更新 GET /v1/drivers 以也列出启用的硬件类型。此更改不受 API 版本控制的影响,因为我们允许旧 API 版本使用 driver 字段中的硬件类型

  • 允许 GET /v1/drivers 仅过滤硬件类型或仅经典驱动程序

    更新 GET /v1/drivers/<HW TYPE> 以报告硬件类型信息,包括启用的硬件接口列表。

    此功能受 API 版本更新的保护(像往常一样)。

  • 允许在节点列表 API 中按 <interface_name>_interface 字段过滤节点。

    此功能受 API 版本更新的保护(像往常一样)。

  • 创建一个新的表 conductor_hardware_interfaces 以保存 conductor、硬件类型和可用接口之间的关系。在 conductor 启动时,如果它检测到其他 conductor 具有相同启用的硬件类型的不同接口集,将发出警告日志消息。这还将跟踪每种硬件类型和接口类型组合的默认接口。

    在实时升级期间,这种情况是不可避免的,因此不应导致错误。但是,我们将记录所有 conductor 应该对相同的启用硬件类型具有相同的接口集。

    此表现在不会在 HTTP API 中公开。

弃用

我们计划在 V1 API 中弃用和删除对经典驱动程序的支持。

我们计划弃用和删除树内的经典驱动程序。弃用过程可能很棘手,将在后续规范中介绍。

备选方案

  • 我们可以将接口放在节点上的新 JSON 键下。但是,我们试图摆脱非正式定义的 JSON 键。它还会阻止我们根据特定接口过滤节点。

  • 我们可以创建一个新的 API 端点来更新接口。但这与我们更新 driver 字段的方式不一致。

    我们可以创建一个新的 API 版本,从而防止通过常规节点更新 API 更新 driver,但这将是一个破坏性更改。

  • 我们可以创建一个新的字段 hardware_type,而不是让现有的 driver 字段接受硬件类型。这是以前的提案的一部分,但我们发现它会大大增加复杂性,而没有明确的好处。

  • 我们可以创建一个全新的 API 端点系列,而不是重用 /v1/drivers,例如 /v1/hardware-types。但是,这将需要我们几乎完整地复制所有驱动程序相关的功能,例如驱动程序厂商传递。因此,用户需要根据 driver 字段中的驱动程序类型,弄清楚使用哪个厂商传递端点。

数据模型影响

  • 对于每个接口,创建一个新的节点字段 <interface_name>_interface,最初设置为 NULL

  • 创建一个新的内部表 conductor_hardware_interfaces

    conductor_id - conductor ID(外键到 conductors 表),

    hardware_type VARCHAR(255) - 硬件类型入口点名称,

    interface_type VARCHAR(16) - 接口类型名称(例如 deploy),

    interface_name VARCHAR(255) - 接口实现入口点名称。

    default TINYINT(1) - 布尔值,表示此 interface_name 是否是

    给定 hardware_typeinterface_type 组合的默认值。

    此表将在 conductor 启动时填充,并在删除 conductor 记录时清除。在 conductor 启动时,在 init_host() 期间,conductor 将获取所有已注册 conductor 支持的硬件接口列表,并与自己的配置进行比较。如果两个 conductor 上启用了相同的硬件类型,但启用的接口集不同,这将导致 WARNING 日志消息。启用的硬件类型本身不必匹配(就像今天一样,不同的 conductor 可以具有不同的驱动程序集)。

状态机影响

REST API 影响

  • 更新 GET /v1/drivers

    无论使用哪个 API 版本,都返回经典驱动程序硬件类型

    新的 URL 参数

    • type (字符串,可以是 classicdynamic,可选) - 如果提供,则将结果驱动程序列表限制为仅经典驱动程序硬件类型

    新的响应字段

    type,指示驱动程序是动态还是经典

    此更改受新的 API 版本保护。

  • 更新 GET /v1/drivers/<NAME>

    新的响应字段

    type,指示驱动程序是动态还是经典

    仅对于硬件类型,新的非 None 响应字段

    default_<interface_name>_interface

    给定接口的计算默认实现的入口点名称。

    enabled_<interface_name>_interfaces

    给定接口的已启用实现的入口点名称列表。

  • 更新 GET /v1/drivers/<NAME>/propertiesGET /v1/drivers/<NAME>/vendor_passthru/methods 以及实际的驱动程序供应商 passthru 调用实现

    当请求动态驱动程序时,假定计算出的 vendor 接口实现的默认值,如 配置 中所述。 我们还需要支持非默认实现,但这超出了此大规格的范围。

客户端 (CLI) 影响

“ironic” CLI

  • 更新节点创建命令以接受每个接口一个参数。 示例

    ironic node-create --driver=ilo-gen9 --power-interface=redfish
    

    相同的更改应用于 OSC 插件。

  • 使用 Type 列扩展 driver-list 命令的输出。

  • 使用 --type 参数扩展 driver-list 命令,如果提供该参数,则将驱动程序列表限制为仅经典驱动程序classic 值)或硬件类型dynamic 值)。

  • 使用新引入的字段扩展 driver-show 命令的输出。

“openstack baremetal” CLI

“ironic” CLI 中的更改类似,此处也应用了相同的更改。

RPC API 影响

  • 由于硬件类型经典驱动程序在同一字段中使用,因此对哈希环没有影响。

驱动程序 API 影响

硬件类型

  • 创建一个新的 AbstractHardwareType 类,作为所有硬件类型的抽象基类。 以下是一个简化的示例实现,仅使用电源、部署和检查接口

    import abc, six
    
    @six.add_metaclass(abc.ABCMeta)
    class AbstractHardwareType(object):
        @abc.abstractproperty
        def supported_power_interfaces(self):
            pass
    
        @abc.abstractproperty
        def supported_deploy_interfaces(self):
            pass
    
        @property
        def supported_inspect_interfaces(self):
            return [NoopInspect]
    

    请注意,某些接口(电源、部署)是必需的,而其他接口(检查)不是。 将为所有可选接口提供一个虚拟实现。 根据特定的调用,它将不执行任何操作或引发错误。 对于用户发起的调用(例如,启动检查),将返回错误。 对于内部调用(例如,附加清理端口),将不采取任何操作。

  • 创建一个新的 GenericHardwareType 类,大多数实际的硬件类型类都希望继承该类。 此类将为某些接口插入通用实现

    class GenericHardwareType(AbstractHardwareType):
        supported_deploy_interfaces = [AgentDeploy]
        supported_inspect_interfaces = [NoopInspect, InspectorInspect]
    

    请注意,所有属性都包含类,而不是实例。 还要注意顺序:在此示例中,如果配置中同时启用了这两个实现,NoopInspect 将是默认值。

  • 以下是如何创建硬件类型的示例

    class GenericIpmiHardware(GenericHardwareType):
        supported_power_interfaces = [IpmitoolPower, IpminativePower]
    
    class iLOGen8Hardware(GenericHardwareType):
        supported_power_interfaces = (
            GenericIpmiHardware.supported_power_interfaces
            + [IloPower]
        )
        supported_inspect_interfaces = (
            GenericHardwareType.supported_inspect_interfaces
            + [IloInspect]
        )
    
    class iLOGen9Hardware(iLOGen8Hardware):
        supported_power_interfaces = (
            iLOGen8Hardware.supported_power_interfaces
            + [RedfishPower]
        )
    

注意

这些定义使用类,而不是入口点名称。 这些示例假定所需的类已导入。

注意

为了使这些示例正常工作,必须定义以下入口点

ironic.hardware.types =
    generic-ipmi = ironic.hardware.ipmi:GenericIpmiHardware
    ilo-gen8 = ironic.hardware.ilo:iLOGen8Hardware
    ilo-gen9 = ironic.hardware.ilo:iLOGen9Hardware

ironic.hardware.interfaces.power =
    ipmitool = ironic.drivers.modules.ipmitool:IpmitoolPower
    ipminative = ironic.drivers.modules.ipmitool:IpminativePower
    ilo = ironic.drivers.modules.ilo:IloPower
    redfish = ironic.drivers.modules.redfish:RedfishPower

ironic.hardware.interfaces.inspect =
    inspector = ironic.drivers.modules.inspector:InspectorInspect
    ilo = ironic.drivers.modules.ilo:IloInspect

需要以下配置才能启用这些示例中的所有内容

[DEFAULT]
enabled_hardware_types = generic-ipmi,ilo-gen8,ilo-gen9
enabled_power_interfaces = ipmitool,ipminative,ilo,redfish
enabled_inspect_interfaces = inspector,ilo

驱动程序创建

  • 在启动时,conductor 会实例化所有启用的硬件类型,以及所有为启用硬件类型启用的接口实现。

  • 每次创建节点或从数据库加载节点时,都会创建一个精简的 BareDriver 对象,并将其所有接口都设置在上面。 这类似于网络驱动程序的工作方式。 它被分配给 task.driver,此后一切都像在此规格之前一样工作。

Nova 驱动程序影响

Ramdisk 影响

安全影响

其他最终用户影响

  • 最终用户应逐渐切换到硬件类型

可扩展性影响

性能影响

  • 现在将为每个节点创建一个驱动程序实例,而不是像现在这样为每个 conductor 创建一个。 这将在某种程度上增加每个节点的内存使用量。 我们可能可以在驱动程序类上定义 __slots__ 来减少这种影响。

其他部署者影响

  • 部署者可以设置新的 enabled_hardware_types 选项以启用更多硬件类型。 否则,只有默认硬件类型和已启用的经典驱动程序可用。

  • 部署者还可以设置任何新的 enabled_<INTERFACE>_interfaces 选项以启用已启用的硬件类型的更多接口

开发人员影响

此规格更改了我们期望开发人员编写驱动程序的方式。

  • 一旦此更改生效,将不再接受树内的新的经典驱动程序

  • 开发人员应实现硬件类型接口,以提供 Ironic 的新硬件支持。 内置接口实现将在树内和树外都可以重用。

实现

负责人

  • Dmitry Tantsur (lp: divius, irc: dtantsur)

  • Jim Rollenhagen (irc: jroll)

工作项

  • 创建支持硬件类型的基础类。

  • 创建用于跟踪已启用硬件接口的表。

  • 在 conductor 启动时加载硬件类型并在内部表中记录它们。

  • 创建用于接口的节点字段并在 API 中公开它们。

  • 更新驱动程序 API 以支持硬件类型

  • 为团队直接支持的硬件(即通用的 IPMI 兼容硬件)创建硬件类型。 SSH 驱动程序可能很快会被删除;在这种情况下,它将不会更新。

依赖项

测试

  • 显然会提供单元测试覆盖率。

  • 将创建一个新的 gate 作业,使用动态版本的 IPMI 驱动程序。 我们将努力使其成为 gate 中首选的方法。

  • 用于升级/迁移现有工作负载到新驱动程序的 grenade 测试。

升级和向后兼容性

此重构旨在向后兼容。经典驱动程序将在一段时间内得到支持。 独立的规格将涵盖经典驱动程序的弃用。

我们将建议尽快切换到使用合适的动态驱动程序

升级流程

  1. Ironic 更新到支持动态驱动程序的版本。 客户端使用的 API 版本尚未更新。

  2. 所有节点仍然使用经典驱动程序。 在节点 driver=x_y 上。

  3. 使用旧 API 版本的用户

    • 可以将 driver 设置为经典驱动程序

    • 可以将 driver 设置为硬件类型,这将导致使用具有默认接口集的动态驱动程序

  4. 使用新 API 版本的用户

    • 可以将 driver 设置为硬件类型经典驱动程序

    • driver 设置为真实的硬件类型时,可以设置非默认接口实现

文档影响

  • 记录切换到动态驱动程序

  • 记录创建新的硬件类型

参考资料

初始 etherpad:https://etherpad.openstack.org/p/liberty-ironic-driver-composition

Newton etherpad:https://etherpad.openstack.org/p/ironic-newton-summit-driver-composition