组件的开发和代码编写
更新时间:2024/12/16
在Gitcode上查看源码

在阅读本章节之前,您可以通过《新增一个组件》快速了解如何实现新增组件,以及在新组件中扩展相关接口和硬件适配等。

openUBMC各个组件对外的接口统一由资源协作接口定义。组件是全量功能提供者,来实现资源协作接口。资源协作接口采用D-Bus总线协议实现。各组件可以通过资源协作接口新增、查看或调用各服务提供的可用接口,而无需关注接口在其他组件中的内部实现。在阅读本篇介绍之前,请先了解D-Bus总线协议

除了统一管理各组件对外的接口。openUBMC通过引入错误引擎框架,也建立了一套通用的错误处理机制,将各组件的错误定义统一管理。利用错误引擎框架,各组件只需自动动态将错误信息注册到错误引擎,后续由错误引擎框架接管各组件的错误处理流程。

openUBMC将资源协作接口和错误引擎统一由mdb_interface组件仓管理。开发者可以通过依赖mdb_interface组件仓,使用资源协作接口和错误引擎。也可以通过mdb_interface组件仓将自定义的错误机制添加到错误引擎中,或将自己所开发组件的对外接口添加到资源协作接口中,暴露给其他组件使用。开发者可参考《mdb_interface》 快速了解mdb_interface组件仓。

下面简单介绍组件开发过程中,如何利用mdb_interface组件仓实现查看、新增和调用资源协作接口。以及组件开发过程中,错误引擎的使用。

资源协作接口查看

D-Bus总线进行交互的命令行工具busctl,提供了许多管理和控制D-Bus总线的功能。使用busctl命令,可以查看和控制D-Bus总线上的所有对象,接口和方法。因此,开发者可以在支持openUBMC的环境中,使用busctl命令查看资源协作接口信息。 **注意:**若环境中使用的是stablerc类型的制品包,则不支持busctl命令,只有dev类型的制品包才支持busctl命令。

常用busctl命令如下:

  • 查看D-Bus用户总线上的所有服务信息

    shell
    busctl --user tree
  • 查看D-Bus用户总线上指定服务的对象

    shell
    busctl --user tree <service>
  • 查看D-Bus用户总线上指定对象的接口

    shell
    busctl --user introspect <service> <object>
  • 监视D-Bus用户总线上的消息传递

    shell
    busctl --user monitor [service] [object]
  • 查看D-Bus用户总线上指定对象的属性

    shell
    busctl --user get-property <service> <object> <interface> <property>
  • 查看D-Bus用户总线上指定对象的属性

    shell
    busctl --user set-property <service> <object> <interface> <property> <SIGNATURE ARGUMENT...>
  • 使用D-Bus用户总线进行方法调用

    shell
    busctl --user call <service> <object> <interface> <method> [SIGNATURE [ARGUMENT...]]

这里以chassis服务的/bmc/kepler/chassis/MicroComponent接口为例,使用以下命令查看该接口上挂载的所有属性和方法等。

shell
busctl --user introspect bmc.kepler.chassis /bmc/kepler/chassis/MicroComponent

命令执行成功,会返回以下信息(由于篇幅限制,这里只展示部分返回信息):

shell
NAME                                   TYPE      SIGNATURE   RESULT/VALUE                   FLAGS
bmc.kepler.MicroComponent              interface -           -                              -
.HealthCheck                           method    a{ss}       i                              -
.Author                                property  s           "openUBMC"                       const
.Description                           property  s           "chassis management component" const
.License                               property  s           "Mulan PSL v2"                 const
.Name                                  property  s           "chassis"                      const
.Pid                                   property  i           2543                           const
.Status                                property  s           "InitCompleted"                -
.Version                               property  s           "1.60.16"                      const
bmc.kepler.MicroComponent.ConfigManage interface -           -                              -
.Backup                                method    a{ss}s      a(ss)                          -
.Export                                method    a{ss}s      s                              -
.GetTrustedConfig                      method    a{ss}       s                              -
.Import                                method    a{ss}ss     -                              -
.Recover                               method    a{ss}a{sb}  -                              -
.Verify                                method    a{ss}s      s                              -

如上述返回信息所示,表头信息NAME表示此列为接口名、属性或方法名;表头信息TYPE表示数据类型;表头信息SIGNATURE表示属性的数据类型或方法的入参数据类型,SIGNATURE类型定义可参考D-Bus Specification官方文档;表头信息RESULT/VALUE表示属性值数或方法的返回值;表头信息FLAGS表示是否可读写。

新增资源协作接口

注意:
在openUBMC中,开发者如需新增D-Bus的接口资源,需要明确新增的属性信息,例如接口所在的服务、对象、以及接口名称、接口下所挂载的方法、属性等,并交由开源SIG组织的评审会议进行评审。评审通过后,可在mdb_interface组件仓中进行资源协作接口配置,并在组件代码仓中实现相应的功能。

openUBMC采用数据模型的方式定义资源协作接口。此数据模型在mdb_interface组件中统一定义资源协作接口及其挂载的公共属性和RPC方法等。组件通过模型定义接口、具体实现接口方法,再由自动生成代码注册资源协作接口上所挂载的RPC方法。由于openUBMC不支持用户对mdb_interface组件仓中的接口定义随意更改,mdb_interface组件仓提供了一个额外的资源协作接口的定义,用于社区演示和开发人员调试学习。在本章节中,将根据此接口定义介绍openUBMC新增一个资源协作接口的代码实现流程。

步骤一:新增资源协作接口定义

通过开源SIG组织评审后,开发人员根据评审结果可在mdb_interface组件仓中新增资源协作接口数据模型的定义。对象和接口的定义分别存放在json/path/json/intf/路径下。其中,在json/path/目录下定义对象以及该对象下所挂载的接口,而接口的具体定义(即接口上所挂载的方法、属性等信息)存放在json/intf/目录下。

为支持本文档的示例演示,openUBMC在资源协作接口定义的组件仓mdb_interface中已经添加了/bmc/demo/MyMDSModel对象定义和在该对象下挂在的bmc.demo.OpenUBMC.Community接口定义。此部分的代码定义如下:

  1. 新增对象定义:开发者需要在mdb_interface组件代码仓的json/path/路径下添加对象定义文件,如新增/bmc/demo/MyMDSModel/对象定义文件json/path/mdb/bmc/demo/MyMDSModel/MyMDSModel.json(此文件路径和对象定义中的path属性对应,将path属性表示的路径中删除参数变量的层级即为该文件在json/path/mdb目录下存放的路径,且创建的文件名与类名一致)。对象定义代码如下:

    json
    {
        "MyMDSModel":{
            "path":"/bmc/demo/MyMDSModel/",
            "interfaces": [
                "bmc.demo.OpenUBMC.Community"
            ]
        }
    }

    若在mdb_interface组件中已经存在此对象定义,开发者只需在该对象定义的interfaces属性中添加需要新增的接口即可。

  2. mdb_interface代码仓的json/intf/路径下新建bmc.demo.OpenUBMC.Community接口的定义文件intf/mdb/bmc/demo/OpenUBMC/Community.json文件(此文件路径与接口定义对应,将interfaces属性表示的值转换为路径表示,即为该文件在json/intf/mdb目录下存放的路径,最后一层为文件名),并添加接口定义信息:

    json
    {
        "bmc.demo.OpenUBMC.Community": {
            "properties": {
                "WelcomeMessage": {
                    "baseType": "String",
                    "description": "OpenUBMC欢迎消息"
                }
            },
            "methods": {
                "GetRepoURL": {
                    "description": "获取OpenUBMC代码仓的URL",
                    "req": {
                        "SercretNumber": {
                            "baseType": "U32",
                            "description": "OpenUBMC的神秘代码"
                        }
                    } ,
                    "rsp": {
                        "OutData": {
                            "baseType": "String",
                            "description": "OpenUBMC的URL"
                        }
                    }
                }
            }
        }
    }
  3. 完成以上步骤后,开发者修改service.json中定义的版本号(即将三段式版本号的最后一段值加1),并在CHANGELOG.md中添加修改日志,便可在mdb_interface代码仓路径下通过以下命令构建出包:

    shell
    bingo build

    构建成功后便实现了在资源协作接口定义中新增/bmc/demo/MyMDSModel/对象定义、并在该对象定义上挂载bmc.demo.OpenUBMC.Community接口定义,此接口定义上挂载WelcomeMessage属性和GetRepoURL方法的定义。

步骤二:自动生成代码

openUBMC采用的模型定义文件简化了开发任务,只需要在组件中的模型描述文件中依赖与组件所需实现的接口定义,便可通过代码自动生成实现相关代码的实现。模型描述文件的详细代码编写可参考《MDS》。这里以my_app组件为例,实现bmc.demo.OpenUBMC.Community接口:

  1. 首先,需要在my_app组件的mds/service.json文件中引用mdb_interface组件依赖:

    json
    {
        ...
        "dependencies": {
            ...
            "build": [
                ...
                {
                    "conan": "mdb_interface/[>=0.0.1]"
                }
            ]
        }
    }
  2. my_app组件的mds/model.json文件中定义该接口。注意接口定义路径和属性、方法类型等与mdb_interface组件仓中的接口定义相同。其中,MyMDSModel为业务模型定义的类名,与path属性对应的文件名保持一致。interfaces表示该类支持的资源协作接口的接口集合。接口定义下的properties属性表示需要挂到资源协作接口下的属性的集合。MyMDSModel属性下的properties表示此类的私有属性,无需挂到资源协作接口下,且不能与挂到资源协作接口下的属性和属性别名重名。

    json
    {
        "MyMDSModel": {
            "path": "/bmc/demo/MyMDSModel/${id}",
            "interfaces": {
                "bmc.demo.OpenUBMC.Community": {
                    "properties":{
                        "WelcomeMessage": {}
                    }
                }
            },
            "properties": {
                "SecretNumber": {
                    "baseType": "U32"
                }
            }
        }
    }
  3. 以上已经完成my_app组件的类定义数据模型。根据BMC Studio CLI提供的命令便可以根据上述数据模型定义文件自动生成相关代码。

    shell
    cd <my_app组件目>
    bingo gen

    命令执行成功,会在my_app组件目录下生成gen文件夹,此文件夹下包含了多个自动生成的代码文件。自动生成代码实现了对象注册的回调函数、服务的初始化、接口和属性监听的订阅函数等。关于自动生成的代码文件介绍可参考代码自动生成

步骤三:组件实现

自动生成的代码文件my_app/service.lua为组件的基类,开发者可以通过引入此基类,在源码文件中注册并实现接口。示例:

lua
local class = require 'mc.class'                                       -- Lua开发框架提供的增强类型系统
local service = require 'my_app.service'                               -- 自动生成代码生成的组件基类
local log = require 'mc.logging'
local object_manage = require 'mc.mdb.object_manage'

local app = class(service)                                             -- 创建一个组件类型

function app:ctor()                                                    -- 组件的构造函数
end
function app:init()                                                    -- 组件的初始化函数
    app.super.init(self)                                               -- 先调用基类的初始化函数
    self.my_mds_model = self:CreateMyMDSModel(1, function(object)      -- 创建一个mds对象实例
        object.ObjectName = "MyMDSModel_1"                             -- 在回调函数中进行对象的属性赋值
        object.WelcomeMessage = "Hello OpenUBMC!"
        object.SecretNumber = 330
    end)
    log:notice("SecretNumber")
    log:notice(self.my_mds_model.SecretNumber)

    self:register_rpc_methods()
end

function app:register_rpc_methods()                                    --通过基类中的回调函数实现接口上挂载的RPC方法
    self:ImplMyMDSModelCommunityGetRepoURL(function(obj, ctx, ...)
        log:notice("welcome to the method")
        return 'https://gitcode.com'
    end)
end

return app

在openUBMC中,Lua开发框架定义了类实例化时,会自动执行此类的构造函数ctor(),再执行此类的初始化函数init()

步骤四:组件构建

完成以上步骤,新增RPC方法的代码已经全部完成。开发者修改my_app代码仓中service.json文件中定义的版本号(将三段式版本号的最后一段值加1,即my_app组件版本号从0.0.1修改为0.0.2),并在CHANGELOG.md中记录修改日志,便可在my_app代码仓的路径下执行以下命令构建出my_app的组件包:

shell
bingo build

构建成功,控制台会输出组件包名my_app/0.0.2@openUBMC.dev/dev。开发者可以在root/.conan/data路径下查看构建出的组件Conan包。

步骤五:产品构建

获取组件的Conan包后,开发者在openUBMC的制品仓manifest中添加此组件,便可构建出openUBMC的制品包。 这里开发者需要先拉取manifest组件仓。根据环境机型在对应的manifest.yml文件中添加组件包依赖。例如,这里选择TaiShan200_2280v2机型的环境。则应该在manifest组件仓的build/product/BMC/openUBMC/manifest.yml文件中添加my_app依赖:

yml
dependencies:
  - conan: "my_app/0.0.2@openUBMC.dev/dev"
  ...

执行完以上步骤,在manifest组件仓目录下执行以下命令构建TaiShan200_2280v2机型的openUBMC制品包:

shell
bingo build

命令执行成功,便可在manifest组件仓的output目录下生成制品包rootfs_TaiShan200_2280v2.hpm。之后,开发者便可使用此制品包更新openUBMC环境,并进行验证。

示例:

  1. 在支持openUBMC的环境中升级此制品包后,可执行以下命令查看/bmc/demo/MyMDSModel对象上挂载的所有接口:
    shell
    busctl --user introspect bmc.kepler.my_app /bmc/demo/MyMDSModel/1
  2. 在支持openUBMC的环境中升级此制品包后,可执行以下命令调用bmc.demo.OpenUBMC.Community接口的GetRepoURL方法:
    shell
    busctl --user call bmc.kepler.my_app /bmc/demo/MyMDSModel/1 bmc.demo.OpenUBMC.Community GetRepoURL a{ss}u 0 1

注意:
为了提升效率,openUBMC支持组件根据组件的依赖关系有序拉起,且根据不同子系统并行被拉起。但根据以上的代码实现,my_app组件将会在环境中单独被拉起。若my_app和其他组件被拉起的顺序有要求时,可以修改hica组件仓,并在制品仓中修改制品中依赖的hica版本,将my_app组件放入子系统进程中启动。之后在manifest仓中构建出包便支持组件并行有序被拉起。关于hica组件的修改请参考《hica》

调用资源协作接口RPC接口

在开发组件的业务代码时,不可避免的需要调用其他组件的RPC方法。下面简要介绍如何在另一个新增组件new_app中调用资源协作接口上挂载的bmc.demo.OpenUBMC.Community接口的GetRepoURL方法:

步骤一:引用接口

首先在new_app组件的service.json文件的required属性值中添加依赖的接口。这里,path值为“*”,表示会在资源协作接口上全局搜索此接口。当然,开发者也可以通过定义path属性,引用特定路径下的接口。

json
"required": [
    {
        "path": "*",
        "interface": "bmc.demo.OpenUBMC.Community"
    }
]

步骤二:代码自动生成

执行bingo gen命令后,在/gen路径下自动生成代码。关于自动生成代码的详细介绍请参考《代码自动生成》。其中,gen/new_app/client.lua是依据service.json以及mdb_interface接口定义文件自动生成的代码文件。根据service.json文件required部分定义的接口依赖生成客户端资源协作接口的接口方法调用、对象获取和信号订阅等函数。例如此文件中定义了获取bmc.demo.OpenUBMC.Community接口对象的方法:

lua
function new_app_client:GetCommunityObjects()
    return get_non_virtual_interface_objects(self:get_bus(), 'bmc.demo.OpenUBMC.Community', true)
end

步骤三:代码开发

通过以上自动生成代码,开发者在编写业务代码时,可以使用client中相应的函数获取接口对象,并通过接口对象调用此对象下挂载的RPC方法。这里以new_app组件业务代码的入口文件/src/lualib/new_app_app.lua为示例,编写以下代码实现:

lua
local class = require 'mc.class'
local service = require 'new_app.service'
local new_app_client = require 'new_app.client'
local log = require 'mc.logging'
local context = require 'mc.context'


local new_app = class(service)

function new_app:ctor()

end

function new_app:init()
    self.super.init(self)
    -- 获取接口对象
    local objs = new_app_client:GetCommunityObjects()

    -- 调用接口对象下挂载的GetRepoURL()方法
    local err , repoURL = self.bus:pcall("bmc.kepler.my_app","/bmc/demo/MyMDSModel/1", "bmc.demo.OpenUBMC.Community","GetRepoURL", "a{ss}u", context.new(), 1)
    log:notice(repoURL)
end

return new_app

以上已经实现在组件中如何调用资源协作接口上的接口和调用资源协作接口RPC方法。

错误引擎

在openUBMC中,组件对外的错误由mdb_interface组件统一管理。错误定义的json文件存放在mdb_interface代码仓的message目录下。message目录结构如下:

shell
.
├── base.json         -- Redfish标准错误
└── custom.json       -- openUBMC自定义错误

开发者可以按需在message目录下,新增错误定义的目录和文件。

步骤一:代码自动生成

开发者在使用错误引擎前,首先要确认在组件中引入了mdb_interface依赖。即在service.json文件中添加build依赖。然后,在本地组件仓路径下构建后可在temp/opt/bmc/lualib/messsages目录下获取到部署态的代码。temp/opt/bmc/lualib/messsages目录结构如下:

shell
.
├── base.lua
├── custom.lua
└── init.lua

其中部分代码样例如下:

lua
M.PropertyValueTypeErrorMessage = {
    Original = {
        Description = [=[Indicates that a property was given the wrong valu]=] ..
            [=[e type, such as when a number is supplied for a pr]=] .. [=[operty that requires a string.]=],
        Message = [=[The value %1 for the property %2 is of a different]=] .. [=[ type than the property can accept.]=],
        Severity = [=[Warning]=],
        NumberOfArgs = 2,
        ParamTypes = {[=[string]=], [=[string]=]},
        Resolution = [=[Correct the value for the property in the request ]=] ..
            [=[body and resubmit the request if the operation fai]=] .. [=[led.]=],
        HttpStatusCode = 400,
        IpmiCompletionCode = [=[0xFF]=],
        SnmpStatusCode = 7,
        TraceDepth = 0
    },
    Name = 'PropertyValueTypeError',
    Format = [=[The value %s for the property %s is of a different]=] .. [=[ type than the property can accept.]=],
    BacktraceLevel = 0,
    Severity = log.WARN,
    HttpResponse = 400,
    IpmiResponse = 0xFF,
    RegistryPrefix = 'Base'
}
---@return Error
function M.PropertyValueTypeError(val1, val2)
    return create_error(M.PropertyValueTypeErrorMessage, val1, val2)
end

该样例中定义了类型错误函数。开发者在代码编写时,可直接调用此方法对错误信息抛出异常。

步骤二:错误引擎的应用

以上述样例中抛出属性类型不一致错误的M.PropertyValueTypeError函数为例。使用错误引擎对上述添加的bmc.demo.OpenUBMC.Community接口的GetRepoURL方法的入参参数类型进行检查。由于资源协作接口中定义的GetRepoURL方法的入参为无符号整型,当传入参数类型为非整型时,应该抛出参数类型不一致错误。这里在此接口的实现组件my_app中,添加此错误引擎的使用。 在my_app组件的业务代码入口文件/src/lualib/new_app_app.lua中编写RPC方法的实现,并在实现代码中添加错误引擎。代码示例如下:

lua
local base_messages = require 'messages.base'

function my_app:MyMDSModelCommunityGetRepoURL(attribute, value)
    if type(value) ~= 'number' then
        error(base_messages.PropertyValueTypeError(value, 'Attributes/' .. attribute.AttributeName))
    end
    return 'https://gitcode.com'
end

与北向接口直接对接的方法中,接收到其他组件抛出的错误,可在中间层报错,将错误转换后再抛出错误异常。例如,在new_app组件中修改调用GetRepoURL方法的函数体。代码示例如下:

lua
local base_messages = require 'messages.base'

function new_app:MyMDSModelCommunityGetRepoURL(obj, ctx, attribute, value)

    local err , repoURL = self.bus:pcall(obj.service,obj.path, obj.interface,"GetRepoURL", "a{ss}u", ctx, 1)
    if err then
        if err.name == base_messages.PropertyValueTypeError.Name then
            error(base_messages.PropertyValueTypeError(value, 'Attributes/' .. attribute.AttributeName))
        end
    end
end