在带外管理业务编写中,除了从CPLD寄存器中读取/写入某个值以外,还有与MCU、芯片等通过复杂协议进行通讯的场景,如SMBus、MCTP等。通过这些复杂协议,BMC可以传输更多的数据,和提供更复杂的带外管理能力。但复杂协议带来的协议管理成本也逐渐上升。openUBMC为了简化复杂协议的管理逻辑,让开发者可以更加聚焦在设备管理业务,开发了一套硬件协议管理库libmgmt_protocol。组件可以通过依赖这个库来简化协议的开发成本。
在network_adapter组件中有大量的使用案例,可以参考组件的实现细节。
注意:
仅适用于Lua开发框架。
协议支持
| 名称 | 介绍 | 传输通道 | 传输组件 |
|---|---|---|---|
| std_smbus | 通用smbus协议 | i2c | hwproxy |
| smbus | 私有smbus协议 | i2c | hwproxy |
| smbus_5902 | 5902专用smbus协议 | i2c | hwproxy |
| smbus_postbox | NVIDIA SMBus SMBPBI协议 | i2c | hwproxy |
| ncsi_standard | NCSI Over MCTP协议的标准协议部分 | MCTP over PCIE VDM | mctpd |
| ncsi_huawei | NCSI Over MCTP协议的华为OEM协议 | MCTP over PCIE VDM | mctpd |
| ncsi_mellanox | NCSI Over MCTP协议的Mellanox OEM协议 | MCTP over PCIE VDM | mctpd |
| nvme_mi_standard | nvme-mi Over MCTP协议的标准协议部分 | MCTP over PCIE VDM | mctpd |
目录结构
├─ libmgmt_protocol/ -- libmgmt_protocol库
│ ├─ init.lua -- libmgmt_protocol入口文件,提供对外函数解析协议配置文件
│ ├─ protocol/ -- 协议文件夹
│ │ ├─ protocol.lua -- 协议interface文件,无实际作用
│ │ ├─ ncsi_huawei.lua -- NSCI Over MCTP 华为OEM协议
│ │ ├─ ncsi_standard.lua -- NSCI Over MCTP NSCI标准协议
│ │ ├─ ncsi_mellanox.lua -- NSCI Over MCTP Mellanox OEM协议
│ │ ├─ nvme_mi_standard.lua -- NCME-MI标准协议
│ │ ├─ smbus_5902.lua -- 5902专用smbus协议
│ │ ├─ smbus_postbox.lua -- NVIDIA SMBPBI协议
│ │ ├─ smbus.lua -- 私有smbus协议
│ │ ├─ std_smbus.lua -- 标准smbus协议
│ ├─ transport/ -- 协议传输层,用于封装协议传输函数
│ │ ├─ hw_communicator.lua -- 用于创建单次访问/轮询
│ │ ├─ scheduler.lua -- 用于轮询访问硬件的插件
│ ├─ common/
│ │ ├─ init.lua -- 用于共享常用协议解析工具
│ │ ├─ bs_helper.lua -- 通用bitstring格式
│ │ ├─ lldp_tlv_parser.lua -- lldp tlv报文解析函数
│ │ ├─ vdpci_lldp_parser.lua -- vdpci lldp专属报文解析函数使用方法
由于打包到lualib,所有组件可以直接require。
-- require libmgmt_protocol 库
local libmgmt_protocol = require 'libmgmt_protocol'
-- 各组件定义所属硬件的访问协议配置文件
local config_obj = require 'path.to.config.obj'
-- 根据文件创建可访问对象
-- 若配置有问题这里会抛异常
local obj = libmgmt_protocol.device_spec_parser(config_obj)
-- 根据配置文件的具体值获得对应硬件属性值
-- 对于单次访问返回值的属性,需要调用:value()获取具体的值
-- 若属性不存在或者无法获取属性值:value()会返回nil
-- 入参为配置请求数据的补充,若冲突则不会覆盖配置中的请求数据
local on_demand_property = obj:OnDemandProperty({data = 'some data'}):value()
-- 对于轮询访问的属性,返回体为scheduler对象
-- 若请求数据不需要变化,则不用传入任何数据
local on_schedule_property_scheduler = obj:OnScheduleProperty()
-- scheduler不会自动开启,需要手动调用:start()开启,同时会返回第一次获取的数据
local on_schedule_property = on_schedule_property_scheduler:start()
-- scheduler支持订阅数据变更信号,当获取数据不同于scheduler缓存数据时,会发送
-- on_data_change信号和新的数据
on_schedule_property_scheduler.on_data_change:on(function(data)
print('new data received!')
end)
-- scheduler支持订阅错误信号,当每次轮询获取数据失败(nil)时,会发送on_error信号
on_schedule_property_scheduler.on_error:on(function()
error('unable to read data from hardware!')
end)
-- scheduler支持更换入参,调用后下一次轮询时便会使用新的参数
on_schedule_property_scheduler:update_params({
name = 'another_property_name', -- 新属性名
protocol = 'new protocol', -- 新协议名称(暂不支持加载未使用过的协议)
request = {...}, -- 协议所需的入参
})
-- scheduler支持更换轮询周期,调用后下一次轮询时便会使用新的参数
on_schedule_property_scheduler:set_period(10) -- 10s轮询一次
-- scheduler会根据配置的轮询周期一直访问数据,在不使用时需要调用:deconstruct()停止轮询访问
on_schedule_property_scheduler:deconstruct()
--对于不存在的属性,调用scheduler的任意函数都不会起效,用于在不确定属性是否存在时
local not_exist_property = obj:NotExistProperty()
not_exist_property:start() -- no op
not_exist_property.on_data_change:on(function()
assert('never reach here') -- 此行永远无法到达
end)
not_exist_property.on_error:on(function()
assert('never reach here') -- 此行永远无法到达
end)
not_exist_property:deconstruct() -- no op属性访问文件配置
属性访问文件用于配置硬件属性访问所需的固定参数,由协议依赖结构体和多个属性结构体组成
目前仅支持使用Lua进行配置。
local a_network_adapter = {
protocol_dependencies = {
smbus = { -- 所用通过I2C的协议都需要传入ref_chip对象,
ref_chip = nil, -- 通过hwproxy对硬件进行访问
buffer_len = 64, -- 协议支持的最大字节数
},
ncsi_huawei = { -- 所有通过mctp的协议都需要传入endpoint对象,
endpoint = nil -- 通过mctpd对硬件进行访问
}
},
properties = {
...
}
}
return a_network_adapter配置可以放在组件的任意地方,推荐单独存放,以便于后续迭代。
属性配置
每个属性配置由属性名为键,值由协议名、访问方法、请求体和响应函数构成
properties = {
ChipTemp = { -- 属性名,也是属性访问接口, obj:ChipTemp()
protocol = 'smbus', -- 协议名,必须是已支持的协议
action = 'on_schedule', -- 单次访问还是轮询访问
period_in_sec = 2, -- 轮询访问的周期,单位为秒
request = { -- 协议所需的请求数据,具体格式由各协议检查
opcode = 0x3, -- smbus协议需要的opcode
expect_data_len = 2 -- smbus需要的返回数据长度
},
response = function(data) -- 返回数据解析函数,会去掉协议中协议的部分,data仅包括具体数据
-- 仅在硬件正常返回响应时才会调用此函数,有错误时不会调用此函数
local r = bs.new([[<<temp:16>>]]):unpack(data, true) -- 支持使用bitstring进行二进制解包
return r.temp -- 返回值为obj:ChipTemp()的返回值
end
},
MacAddressNCSI = {
protocol = 'ncsi_huawei',
action = 'on_demand', -- 单次访问,单次访问不需要配置period_in_sec
request = {
huawei_cmd_id = 0x1,
sub_cmd_id = 0x0
},
response = function(data)
local r = bs.new([[<<
_:8,
mac_address_count:16/big,
_:16, # libmgmt_protocol提供一些通用的bitstring格式,例如MAC_Address
mac_addrs:8/MAC_ADDRESS
>>]], libmgmt_protocol.common_bs_helper):unpack(data, true) -- 在创建bs的时候直接传入即可
return r.mac_addrs[1].mac_address
end
}
}NOTE
更复杂配置可参考network_adapter组件仓src/lualib/hardware_config下的配置
smbus协议请求体参数说明
通用smbus、私有smbus、5902专用smbus等i2c相关协议,在request参数实现上较为灵活。详细说明如下:
expect_data_len
- -1:读请求,响应为不定长度。按最大单帧长度读
- > 0 :可能是读请求;也可能是写请求,回读写响应。按expect_data_len和最大单帧长度的较小值读
- = 0或nil:如果有data字段,则为写请求,不回读写响应。按data长度和最大单帧长度的较小值写;如果无data字段,理论上无此场景,按最大单帧长度读或写
batch_write
指定为true时可调用框架的数据批量写入函数,常用于固件升级场景。正常情况下必须指定data
write_without_read
指定为true时表示写请求不回读写响应。正常情况下必须指定data且不指定expect_data_len
offset
指定为-1时表示在多帧写请求的场景下,请求报文的offset字段固定填充0
align_len
若指定该参数,在多帧写请求的场景下,如果最后一帧不足align_len将按align_len补足'\x00'
max_frame_len
可指定某条命令的最大单帧长度
data_object_index
指定被管理芯片的索引,用于单个MCU管理多个其他芯片的场景
参数支持情况详见各协议的request_params_template字段
通用协议解析库
框架提供了通用的bitstring格式和通用的协议响应数据解析函数,用于解析协议返回的响应数据。
通用bitstring格式
通用bitstring格式存于include/libmgmt_protocol/common/init.lua, 使用时调用libmgmt_protocol.common_bs_helper
目前支持的格式:
| 名称 | bitstring名称 | 返回格式 |
|---|---|---|
| Mac地址 | MAC_ADDRESS | {mac_address = '00:11:22:33:44:55'} |
| Ipv4地址 | IPV4 | {ipv4 = '192.168.1.255'} |
通用响应数据解析函数
目前支持的函数:
| 名称 | 介绍 | 入参 | 返回格式 |
|---|---|---|---|
create_array_parser | 用于解析bitstring数组 | (单个bitstring定义字符串, 单个数据长度) | 表数组 |
LLDP TLV报文解析函数
基于IEEE 802.1ab标准协议解析LLDP TLV报文
无法解析的报文会抛异常,请使用pcall
目前支持的LLDP TLV报文:
| TLV ID | 名称 | 介绍 | 返回格式 |
|---|---|---|---|
| 1 | ChassisID | 解析ChassisID TLV报文 | {chassis_id:string, chassis_id_subtype:string} |
| 2 | PortID | 解析PortID TLV报文 | {port_id:string, port_id_subtype:string} |
| 3 | TTL | 解析TTL TLV报文 | {ttl:U16} |
| 5 | SystemName | 解析SystemName TLV报文 | {system_name:string} |
| 127 | OrgSpecific | 解析OrgSpecific TLV报文,目前用于获取vlanid | {vlan_id:U16} |
vdpci LLDP报文解析函数
基于华为网卡,通过MCTP的自定义通道从网卡获取的lldp报文专属解析函数
无法解析的报文会抛异常,请使用pcall
新增协议
协议采用继承对象方法,相似协议中相同步骤的部分和差异的部分已继承的方法实现。例如:
smbus和std_smbus的传输方法相同,仅发送协议不同,因此smbus继承std_smbus的大部分函数,除了协议拼装函数ncsi_huawei和ncsi_mellanox都是ncsi_standard的oem命令部分,因此他们继承ncsi_standard的所有函数,仅添加额外拼装和解包步骤
协议必要的函数:
-- @function 构建函数
-- @param request:table
function protocol:ctor(params)
-- params为协议配置文件中的protocol_dependencies,由使用组件定义
-- 例如mctp中需要传入endpoint,smbus需要传入ref_chip和buffer_len
-- 需要与组件配置文件协同
end
-- @function 传输请求函数
-- @param request:table
-- @return binary|boolean|nil
function protocol:send_request(request)
-- request为协议配置文件中各属性的request请求结构体,包括运行态和配置静态的结合体
-- 返回响应体的具体数据,若配置文件中配置了响应解析函数,这里的返回数据为响应解析函数的入参
-- 若请求失败,返回nil
-- 若无需调用响应解析函数,则返回true代表发送成功
end
-- @function 传输请求函数
-- @param request:table
-- @return binary
function protocol:validate_request_params(request)
-- request为协议配置文件中个属性的request请求结构体
-- 在解析配置文件时会调用一次,入参为配置文件中的静态数据
-- 在运行时会再调用一次,入参为运行时传入的动态数据,若为nil则不会调用
-- 返回true才可继续传输,返回false则不会进行传输
end添加全新协议
只要满足上述协议必要的函数即可
添加相似协议
通过利用重写基类函数,可以实现相似协议代码共享。
重写拼装和解包函数
NCSI标准协议和NCSI华为协议差异仅在拼装和解包上,NCSI华为协议需要额外的拼装和解包。
此处参考:
protocol/ncsi_standard.lua
-- ncsi_standard拼装函数只需要拼装ncsi标准协议的部分
function ncsi_standard:construct_request_data(ctx, request)
return req_ctx, req_bin
end
-- ncsi_standard解析函数只需要解析ncsi标准协议的部分
function ncsi_standard:unpack_response_data(ctx, rsp_data_bin)
return ...
end
-- 将协议拼装和解包拆成独立的函数
function ncsi_standard:send_request(request)
local req_ctx, req_bin = self:construct_request_data(ctx, request)
local ok, rsp_data_bin = pcall(self.endpoint.Request, self.endpoint, req_ctx, req_bin, 0)
return self:unpack_response_data(ctx, rsp_data_bin)
end需要新增华为oem的协议封装:protocol/ncsi_huawei.lua
local ncsi_huawei = class(ncsi_standard)
-- ncsi_huawei重写基类封装函数,封装自己的部分,再调用基类封装函数
function ncsi_huawei::construct_request_data(ctx, request)
local request_with_ncsi_huawei_info = ...
return ncsi_huawei.super.construct_request_data(ctx, request_with_ncsi_huawei_info)
end
-- ncsi_huawei重写基类解析函数,代替基类函数直接返回自己的解析数据
function ncsi_standard:unpack_response_data(ctx, rsp_data_bin)
return data
end重写硬件访问函数
smbus_5902跟std_smbus封装方法一致,但是在硬件访问时需要多一些判断
protocol/std_smbus.lua
-- std_smbus硬件访问使用hwproxy的WriteRead函数
function std_smbus:send_and_receive(data, len)
return pcall(function()
return self.ref_chip:WriteRead(ctx:new(), data, len)
end)
end
-- 将硬件访问拆成独立的函数
function std_smbus:send_request(request)
...
local ok, rsp_bin = self:send_and_receive(data, len)
...
endprotocol/smbus_5902.lua
local smbus_5902 = class(std_smbus)
-- smbus_5902要求在发送之前检查硬件是否可以访问,所以必须将Write和Read单独发送
-- 可以通过重写基类硬件访问函数,加入自己所需的步骤
function smbus_5902:send_and_receive(data, len)
if self:check_idle() then
self:send(data)
if self:check_idle() then
return self:receive(len)
end
end
end重写参数检查
smbus私有协议和std_smbus的参数检查逻辑一致,但是可接受的参数不一致
参考:
protocol/std_smbus.lua
-- std_smbus允许arg和data参数
local request_params_template<const> = {
opcode = true,
expect_data_len = true,
arg = true,
data = true
}
-- 在构建函数时存储request_params_template
function std_smbus:ctor()
self.request_params_template = request_params_template
end
-- 将request_params_template独立出来
function std_smbus:validate_request_params(request)
for key in pairs(req) do
if not self.request_params_template[key] then
return false
end
end
return true
end参考:
protocol/smbus.lua
local smbus = class(std_smbus)
local request_params_template<const> = {
opcode = true,
expect_data_len = true
}
-- 在构建时会先调用基类构建函数,再调用子类构建函数
-- 在这里存储request_params_template即可覆盖,在调用validate_request_params时
-- 会使用本地的request_params_template
function smbus:ctor()
self.request_params_template = request_params_template
end测试
单元测试以测试独立协议为主,每个协议单元测试必须覆盖协议最基本的函数,包括:
protocol:ctor()protocol:validate_request_params()protocol:send_request()
由于传输层是传入的,所以打桩传输对象即可模拟硬件访问