Lua开发框架
更新时间:2024/12/19
在Gitcode上查看源码

openUBMC的Lua开发框架主要通过mc模块和自动生成代码提供。 当前的编程框架提供多种语言的编程能力,其中Lua语言的支持较为完善,Lua 是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放,很容易被嵌入其他程序中。关于Lua的更多说明,请参考Lua官方文档

Lua类型化编程

openUBMC编程框架基于Lua语言实现了一套基本的类型机制,在Lua表的基础上提供了继承、单例等常用功能。需要注意的是,这一套框架并不是完整的面向对象编程框架,多态、封装、重载、重写等功能并不支持,使用时需要注意,以免导致困惑。

类型化编程可以通过local class = require 'mc.class'来引入

Lua对象生命周期

创建对象通过<类型>.new()方法实现,创建过程会从顶层父类开始顺序向下执行继承链上父类的ctor()构造方法,然后执行自身的ctor()方法,随后如果有定义,会按顺序执行自身的pre_init()方法和init()方法。

注意事项:

  • ctor()方法可以是有参的,初始化参数会同时用来初始化父类,而pre_init()和init()是无参的。

  • pre_init()和init()不保证调用父类对应函数,但子类可以通过.super属性访问父类pre_init()和init()方法,从而在自身的init阶段手动调用。

  • 根据Lua语言语法糖,Lua可以通过.访问成员属性,通过:定义和调用方法。当使用:访问方法时,实参列表第一位会隐含一个self参数,传入对象自身;当使用function obj:func()定义新的成员方法时,形参列表第一位会隐含一个self形参,代表对象自身。

对象继承

如果需要定义一个继承父类base_app的子类app,那么在require class和父类base_app后,可以app.lua进行以下定义。

lua
local app = class(base_app)

function app:ctor()
    app.prop1 = "prop1"
    -- set prop here
end

function app:pre_init()
    self.parent:pre_init()
    -- pre_init here
end

function app:init()
    self.parent:init()
    -- init here
end

function app:func1()
    -- do something
end

-- define functions here
return app

完成该定义后,即可进行local app = require 'app'之后,通过app.new()创建新的子类对象。

单例模式

单例模式是一种设计模式,保证一个类仅有一个实例,并提供一个访问它的全局访问点。单例模式可以避免全局使用的类被频繁地创建与销毁,避免对资源的多重占用。

单例的创建

将对象定义为单例的方法,在完成对象定义后,使用单例将其封装:

lua
-- demo_class_singleton.lua
local singleton = require 'mc.singleton'
local class = require 'mc.class'
demo_class = class()
return singleton(demo_class)

若调用demo_class.new(...)或者demo_class.get_instance(...),此处会由singlenton()将demo_class定义为单例,即demo_class变量全局唯一。

lua
local lu = require 'luaunit'
local demo_class_singleton = require 'demo_class_singleton'

lu.assertEquals(
    demo_class_singleton.new(),
    demo_class_singleton.new()
)

日志框架

日志级别规范

  • Error:程序运行过程中的错误信息,当前处理必须中止,并向上抛出错误,比如无法连接数据库,解析JSON字符串失败、文件创建失败

  • Warning:程序运行过程中有负面影响的信息或事件,当前处理还可以继续,但是可能会有更严重的故障发生,比如内存占占用率高于指定阈值,某个处理过程超过了预期的耗时,flash写入量超标了等

  • Notice:程序运行过程中的重要信息或事件,对程序运行没有负面影响,比如收到了某个关键的系统信号/事件,定时任务成功的完成执行、程序开始监听某个端口等

  • Info:程序运行过程中的一般信息或事件,重要性比Notice级别低,比如某个任务的某个部分完成执行

  • Debug:程序运行过程的详细信息,比如报文数据,函数调用轨迹(入口、出口)等

注意:

  • 当前openUBMC的日志默认只输出notice级别及以上

  • openUBMC所有输出的日志均会落盘,频繁输出日志会导致硬盘寿命快速下降,在频繁打印日志的场景下谨慎使用INFO以上级别。

日志输出级别设置

通过busctl设置

通过busctl设置的命令格式为:

bash
busctl --user call bmc.kepler.<组件> /bmc/kepler/<组件>/MicroComponent bmc.kepler.MicroComponent.Debug SetDlogLevel a{ss}sy 0 <日志级> <生效时>

<组件名>替换为目标组件,指定日志级别和生效时间即可设置日志输出级别

  • 日志级别需要在debug, info, notice, warning, error范围内
  • 生效时间单位为小时

例如如下命令会将network_adapter组件的日志输出级别在2小时内调整为debug级别

bash
busctl --user call bmc.kepler.network_adapter /bmc/kepler/network_adapter/MicroComponent bmc.kepler.MicroComponent.Debug SetDlogLevel a{ss}sy 0 debug 2

通过mdbctl设置

  1. 首先ssh到环境,执行mdbctl进入mdbctl命令行

  2. 使用lsmc命令查看当前启动的组件,并执行attach <组件名>选定要设置的组件,可以通过多次执行该命令选定多个组件

  3. 执行dloglevel <日志级别>来设置已经attach组件的日志输出级别,日志级别需要在debug, info, notice, warning, error范围内,选定的级别及以上级别的日志才会被输出

  4. 如果希望直接在命令行监控日志,可以使用dlogtype <输出类型>命令,输出类型的取值为file, local。若设为local,则日志会直接输出到控制台,适用于直接在环境上调试的场景,此时日志不会被记录到文件;若设为file,则会输出至日志文件

日志记录接口使用指南

调试日志记录接口

接口功能
log:debug(fmt, ...)记录debug级别的调试日志
log:info(fmt, ...)记录info级别的调试日志
log:notice(fmt, ...)记录notice级别的调试日志
log:warning(fmt, ...)记录warning级别的调试日志
log:error(fmt, ...)记录error级别的调试日志

示例:

lua
local log = require 'mc.logging'

-- 推荐以%s对number类型的变量进行打印,可避免该变量在异常情况下为nil时导致的程序抛错
log:debug('test value:%s', value)
-- 可以直接输出对象
log:debug(data_obj)

特性

  • 组件产生的日志会被记录到APP日志/var/log/app.log。/var/log/framework.log中记录的是openUBMC的框架日志,记录的是组件启动和健康检查等信息

重复日志过滤

连续记录重复的日志,只会记录一个,直到记录的日志内容和之前重复的内容不一致,才会将日志重复的次数和新日志的内容打印出来,例如下述日志在2秒内重复输出了4次,则只会记录一条日志

log
2024-12-20 02:20:03.117404 event NOTICE: object_manage.lua(222): add objects callback, path: /bmc/kepler/ObjectGroup/0101, position: 0101, life
 cycle id: 1, count: 15, took 420ms [repeated 4 times in 2s][flush]

日志记录还有风暴抑制机制,如果10秒内超过500条日志会触发风暴抑制,不能再记录日志,10秒后恢复

针对不需要限流的场景提供了不限流日志接口:

接口功能
log:debug_easy(fmt, ...)不限流记录debug级别的调试日志
log:info_easy(fmt, ...)不限流记录info级别的调试日志
log:notice_easy(fmt, ...)不限流记录notice级别的调试日志
log:warning_easy(fmt, ...)不限流记录warning级别的调试日志
log:error_easy(fmt, ...)不限流记录error级别的调试日志

操作日志记录接口

操作日志位置:/var/log/operation.log

该日志为系统操作日志,只记录设置类和用户交互,用于进行操作防抵赖

接口功能
log:operation(initializer, executor, fmt, ...)记录操作日志
log:system(system_id):operation(initializer, executor, fmt, ...)记录区分multihost的操作日志

维护日志记录接口

维护日志位置:/var/log/maintenance.log

该日志用于记录系统运行过程中的维护事件

接口功能
log:maintenance(level, fault_code, fmt, ...)记录维护日志
log:system(system_id):maintenance(level, fault_code, fmt, ...)记录区分multihost的维护日志

运行日志记录接口

运行日志位置:/var/log/running.log

运行日志包括系统运行过程中的异常状态、异常动作、系统进程运行过程中的关键事件和系统资源占用的相关信息等

接口功能
log:running(level, fmt, ...)记录运行日志
log:system(system_id):running(level, fmt, ...)记录区分multihost的运行日志

安全日志记录接口

安全日志位置:/var/log/security.log

安全日志需要记录系统运行过程出现的登录认证、帐户管理、访问控制、网络攻击等安全事件

接口功能
log:security(fmt, ...)记录安全日志
log:system(system_id):security(fmt, ...)记录区分multihost的安全日志

串口日志记录接口

用于在串口输出日志

接口功能
log:debug_printf(fmt, ...)记录debug级别串口日志
log:info_printf(fmt, ...)记录info级别串口日志
log:notice_printf(fmt, ...)记录notice级别串口日志
log:warn_printf(fmt, ...)记录warning级别串口日志
log:error_printf(fmt, ...)记录error级别串口日志

mdbctl日志记录接口

用于在mdbctl命令行中输出日志

接口功能
log:mdbctl_log(fmt, ...)记录日志到mdbctl终端(前提:mdbctl中必须attach该组件,日志才是显示在mdbctl终端)

C语言日志接口

有lua封装C场景,...... C语言日志注意点

  • DT场景下,C语言日志会打印到/dev/shm/debug_log_for_test文件

  • 当前C日志接口改变日志级别没有提供对外接口,只能通过代码修改

C语言日志级别修改示例

c
#include "logging.h"

set_debug_log_level(DLOG_DEBUG);

日志记录后端

Rsyslog后端

目前,框架日志(framework.log)和调试日志(app.log)都是通过Rsyslog进行记录。

简介

rsyslog是linux系统中用来实现日志功能的服务。默认已经安装,并且自动启用。Rsyslog 是一个 syslogd 的多线程增强版,依然基于Syslog协议完成系统日志的处理转发。

Rsyslog相关文件路径和重启方式

可执行文件路径:/usr/sbin/rsyslogd

主配置文件路径:(包含openUBMC使用的主要日志文件配置,例如app调试日志):/etc/rsyslog.conf

自定义配置文件路径(包含一些动态生成的配置):/etc/rsyslog.d/*.conf

重启方式: BMC使用systemctl管理日志服务,重启Rsyslog服务可以运行命令:systemctl restart rsyslog

Lua编程对接C/C++

C/C++代码在Lua代码中的调用方式如下:

  • 在C/C++文件目录下创建和编写CMakeLists.txt

  • 在C/C++文件中添加Lua接口

c
#define LUA_LIB
#define GNU_SOURCE

#include <lua.h>
#include <lauxlib.h>

static int l_do_sth_in_c_func(lua_State *L) {
    // 从Lua虚拟机解析出C类型变量
    const gchar *str_param = (gchar *)luaL_checkstring(L, 1);
    guint32 uint_param = (guint32)luaL_checkinteger(L, 2);
    gboolean boolean_param = (gboolean)luaL_checkinteger(L, 3);
    // 传入参数,调用C函数
    gint32 ret = c_func(str_param, uint_param, boolean_param);
    // 将返回值传回Lua
    lua_pushinteger(L, ret);
    return 1;
}

LUAMOD_API int luaopen_demo_lib_intf(lua_State *L)
{
    luaL_checkversion(L);
    // 注册Lua函数名称
    luaL_Reg l[] = {
        {"do_sth_in_c_func", l_do_sth_in_c_func},
        {NULL, NULL},
    };
    luaL_newlib(L, l);
    return 1;
}
  • 修改模块名目录下的CMakeLists.txt

在其中加入add_subdirectory("src/lualib-src"),使构建时能够编译C/CPP代码

bash
add_subdirectory("src/lualib-src")

install(DIRECTORY src/lualib DESTINATION ${APP_INSTALL_DIR} OPTIONAL)
install(DIRECTORY include DESTINATION opt/bmc/lualib OPTIONAL)
  • 编译C库

执行build来编译C库接口代码,可以在CMakeList.txt指定的install路径下找到C库对应的.so文件

  • 调用C库

编译完成的.so文件可以在Lua代码中调用,例如

lua
-- 导入C库到Lua代码中
local demo_lib_intf = require "demo_lib_intf"
-- 调用C库代码
demo_lib_intf.do_sth_in_c_func("test", 0, true)

持久化

MDS配置持久化方式

本地持久化/远程持久化

类定义中配置tableLocation为Local表示本地持久化,不配或者配置其它取值时为远程持久化。

  • 本地持久化指的是组件直接操作组件数据库进行持久化

  • 远程持久化指的是组件通过操作内存数据库,框架利用钩子捕捉到变更之后通过rpc调用发送到persistence服务进行集中持久化

资源协作接口属性可以配置本地持久化,但目前ORM机制设置资源协作接口对象属性值自动持久化的功能只支持远程持久化,如果配置了资源协作接口属性本地持久化则需要自行管理资源协作接口属性值和持久化数据之间的同步。

持久化类型

类定义中配置tableType表示类所有属性的持久化类型; 属性持久化需要在属性定义中的usage字段配置。

持久化类型可选值如下:

持久化类型描述
TemporaryPer临时持久化(复位丢失,进程重启需要保留的数据)
TemporaryPerRetain临时持久化且对象删除时保留
ResetPer复位持久化(掉电丢失,复位需要保留的数据)
ResetPerRetain复位持久化且对象删除时保留
PoweroffPer掉电持久化(恢复出厂设置丢失,掉电需要保留的数据)
PoweroffPerRetain掉电持久化且对象删除时保留
PermanentPer永久持久化(恢复出厂设置需要保留的数据)

注意事项

  • 永久持久化不支持本地持久化

  • 永久持久化总共可写空间只有2M,只有少数量小、稳定不变的数据例如MAC地址应该配置永久持久化

  • 掉电持久化数据需要从写入量和写入频率考虑对flash寿命的影响

  • 本地持久化只支持用tableType配置整表的持久化类型,属性配置的持久化类型无效

  • 本地持久化没有配置tableType时,默认为PoweroffPer(掉电持久化)

  • 远程持久化如果在tableType和属性的usage中同时配置了持久化类型,则以属性的持久化类型为准

  • 远程持久化如果没有配置持久化类型,则不会持久化,数据只存在内存数据库中

持久化配置方式

表名

在类定义中的tableName字段配置,表示该类通过ORM映射的数据库表名。

属性相关配置

  • primaryKey: 表示该属性在数据库中是否是主键,true表示是主键,false或者不写表示非主键

  • uniqueKey: 表示该属性是否是唯一键,true表示是唯一键,false或者不写表示非唯一键。主键默认是唯一键

  • baseType: 表示属性数据类型,取值包括U8, U16, U32, U64, S8, S16, S32, S64, String等

  • default: 表示属性默认值。如果没有指定,则根据类型生成默认值(数字为0,字符串为"")

  • notAllowNull: 表示该属性在数据库中是否可以为空,true表示不可以为空,false或者不写表示可以为空。主键默认不能为空

  • sensitive: 表示该属性是否是敏感数据,true表示是敏感数据,false或者不写表示非敏感数据。对于指定为敏感数据的属性,一键收集导出持久化数据时,会将属性值用******替代,不暴露真实数据

  • critical: 表示该属性是否是关键数据,true表示是关键数据,false或者不写表示非关键数据。对于指定为关键数据的掉电持久化属性,每次更新数据时会同步写入到备份数据库

约束说明

  1. 不涉及敏感数据、关键数据的应该优先考虑配置为本地持久化 远程持久化需要组件通过rpc发送到persistence服务进行持久化,性能消耗比本地持久化大。 特别是数据量大、数据变更频繁,或者对数据落盘即时性要求高的场景下应该配置为本地持久化。

  2. 配置了持久化类型时必须配置表名(tableName),如果是远程持久化则表名不能与其它组件配置的表名冲突。 远程持久化数据是存放到集中管理的数据库,如果不同组件之间远程持久化表名冲突,则读写数据时会相互干扰。

  3. 废弃的持久化属性不能直接删除,需要配置deprecated: true, 直接删除持久化属性会造成兼容性问题。

  4. 新增和废弃的持久化属性不能是主键或唯一键,并且必须配置允许为空或有默认值。 如果新增和废弃的属性是主键/唯一健/不允许为空并且无默认值,在版本回退等场景下,数据新增和修改时缺少主动赋值,会导致数据操作失败。

  5. 配置了远程持久化的类必须有至少一个属性配置为主键(primaryKey: true)。 远程持久化数据库是列数据库,组件的每条数据按列拆分成多条数据进行存储,按主键值组装成一条完整的数据。如果没有主键会导致有多条数据时数据存取发生错误。

  6. 配置了持久化的类中如果存在usage包含CSR的属性,则主键属性的选取必须保证不同CSR对象之间主键值不会重复。 不同CSR对象之间主键值重复会导致自发现对象添加时发生冲突,对象之间数据相互干扰。

持久化数据库使用

在model.json中完成如下配置后运行自动生成代码,即可在gen目录中的db.lua文件中看到对应的定义

model.json配置:

json
{
    "Account": {
        "tableName": "t_account",
        "tableType": "PoweroffPer",
        "properties": {
            "Id": {
                "baseType": "U8",
                "primaryKey": true
            },
            "UserName": {
                "baseType": "String"
            },
            "Items": {
                "baseType": "Array",
                "items": {
                    "baseType": "String"
                }
            }
        }
    }
}

表格结构:

t_account
IdUserNameItems
1name1["ab", "cd"]
2name2["ab", "cd"]

使用Statement对象操作数据库

使用db:select, db:insert, db:update, db:delete方法可以创建Statement对象

  • :first()方法

通过db:select(Table对象):first()查询符合条件的第一条数据。

  1. 数据存在时返回row对象,可以直接通过.字段名获取字段值

  2. 数据不存在时返回nil

lua
local user1 = db:select(db.Account):where(db.Account.Id:eq(1)):first()

print(user1.UserName) -- name1
  • :all()方法

通过db:select(Table对象):all()查询所有符合条件的数据,返回值是row对象数组(数据不存在时为空)。

lua
local users = db:select(db.Account):all()

print(#users) -- 2
print(users[1].UserName) -- name1
print(users[2].UserName) -- name2
  • :fold()方法

通过db:select(Table对象):fold(回调函数)对查询到的所有row对象执行回调。

lua
local users = {}

db:select(db.Account):fold(function(user)
    print(user.UserName)
    users[#users + 1] = user
end)
  • :order_by()方法

对查询结果进行排序,使用方式为:order_by(列对象, 是否降序),默认是升序(从小到大),可以按多个列排序。

lua
-- 按用户Id升序、用户名降序排序
local users = db:select(db.Account):order_by(db.Account.Id):order_by(db.Account.UserName, true):all()
  • 对查询结果进行截取

跳过x条数据之后截取前y条:limit(y):offset(x),limit必须在offset前面。limit可以单独使用,offset不能单独使用。

lua
-- 查询Id排列第3到第5的用户
local users = db:select(db.Account):order_by(db.Account.Id):limit(3):offset(2):all()
  • :insert()方法
lua
-- 插入一条数据
db:insert(db.Account):value({Id = 3, UserName = 'name3'}):exec()
-- 插入多条数据
local users = {
    {Id = 3, UserName = 'name3'},
    {Id = 4, UserName = 'name4'}
}
db:insert(db.Account):values(users):exec()
  • :update()方法
lua
-- 将Id为1的用户名称改为new_name
db:update(db.Account):value({UserName = 'new_name'}):where({Id = 1}):exec()
-- 将所有用户名称改为abc
db:update(db.Account):value({UserName = 'abc'}):exec()
  • :delete()方法
lua
-- 删除Id为1的用户
db:delete(db.Account):where({Id = 1}):exec()
-- 删除所有用户
db:delete(db.Account):exec()

where条件写法

通过调用Statement对象的:where()方法可以添加条件。支持字典模式、数组模式和Condition模式。

  • 字典模式

where方法传入{字段名 = 字段值}形式的Lua表,表示数据需要与传入的所有字段值匹配。

lua
local users = db:select(db.Account):where({UserName = 'name1'}):all() -- 查询所有UserName为name1的用户

db:delete(db.Account):where({Id = 1, UserName = 'name1'}):exec() -- 删除所有Id为1, UserName为name1的用户
  • 数组模式

where方法传入{{字段名, 字段值}}形式的Lua表,表示数据需要与传入的所有字段值匹配。

lua
local users = db:select(db.Account):where({{'UserName', 'name1'}}):all() -- 查询所有UserName为name1的用户

db:delete(db.Account):where({{'Id', 1}, {'UserName', 'name1'}}):exec() -- 删除所有Id为1, UserName为name1的用户
  • Condition模式

where方法传入一个或多个Condition对象,表示数据需要满足所有传入的条件。

方法sql操作符含义
Field:eq(value)=等于
Field:ne(value)!=不等于
Field:lt(value)<小于
Field:le(value)<=小于等于
Field:gt(value)>大于
Field:ge(value)>=大于等于
Field:like(value)LIKE模糊查询
Field:in_(...)IN匹配多个值
or_(...)OR
lua
local statement = require 'database.statement'
local or_ = statement.or_
-- 将Id为1的用户名称修改为new_name: UPDATE t_account SET UserName='new_name' WHERE Id=1
local users = db:update(db.Account):value({UserName = 'new_name'}):where(db.Account.UserName:eq('name1')):exec()
-- 查询Id在2和5之间的用户: SELECT * FROM t_account WHERE Id >= 2 AND Id <= 5
local users = db:select(db.Account):where(db.Account.Id:ge(2), db.Account.Id:le(5)):all()
-- 查询Id为1,3,5的用户: SELECT * FROM t_account WHERE Id IN (1, 3, 5)
local users = db:select(db.Account):where(db.Account.Id:in_(1, 3, 5)):all()
-- 删除UserName以name开头或者Id大于3的用户: DELETE FROM t_account WHERE UserName LIKE 'name%' OR Id > 3
db:delete(db.Account):where(or_(db.Account.UserName:like('name%'), db.Account.Id:gt(3))):exec()

注意: 使用:like()进行模糊匹配时,如果数据量庞大(超过上万条)可能会耗时较长,并且导致CPU占用率升高

使用实例

下面的例子展示了如何通过db进行持久化数据的查询与删除,内容来自thermal_mgmt组件仓

lua
function fan_obj_manager:clear_persist_fan_info()
    if self.db == nil or self.db.FanInfo == nil then
        return
    end
    local tbl = self.db.FanInfo
    local fan_id = self.persist_fan_id
    local system_id = self.SystemId
    local record = self.db:select(tbl)
        :where(tbl.SystemId:eq(system_id), tbl.FanId:eq(fan_id), tbl.FanPosition:eq(self:get_position()))
        :first()
    if record ~= nil then
        log:notice('[%s]delete persistent info of fan%d', self.ObjectName, fan_id)
        record:delete()
    end
end

ORM机制

ORM对象管理框架是openUBMC的一款当前仍处于实验阶段的进阶型对象管理框架,在持久化能力的基础上进行了扩展,详细说明请参考《ORM对象管理框架使用指南》

bitstring

编程框架提供了一些实用的工具库,如常用工具函数库utils,基于c-json库封装的json,进行比特流编解码的bitstring,此处主要介绍bitstring的使用。

bitstring:二进制编解码模块

功能简介

BMC 开发经常要处理二进制数据,比如 IPMI 协议、SMBIOS 协议等都是二进制协议,硬件驱动读取的基本也是二进制数据。

设计 bitstring 模块的目的,就是尝试模拟 Erlang 的比特语法(Erlang Bit Syntax)来简化 BMC 对二进制的处理。

编码与解码

解码是指从二进制数据中,根据偏移、长度、类型、大小端等配置提取相应的值。 编码是解码的反向操作,把一些值根据配置打包成一串连续的二进制数据。

二进制匹配

什么是二进制匹配?考虑这样的场景: 在二进制解码时,经常需要匹配某些位置是否等于特定协议常量,或某几个字节代表后续编码的长度等。 常规做法就是先解码部分字节,再配合代码实现条件判断、或注册表查表等辅助完成协议解析。

如果将二进制匹配当做二进制编解码的内在特性,那就可以省去额外的代码逻辑。 并且通常要匹配的部分也是协议常量,本身就是协议的一部分,内置在二进制编解码过程更直观。

bitstring模块位语法

bitstring的位语法借鉴了Erlang位语法,请参考Erlang官方文档

<<E1, E2, ..., En>>

每个Ei元素都标示出二进制型或位串的一个片段。单个Ei元素可以有4种形式。

Ei = Value |
    Value:Size |
    Value/TypeSpecifierList |
    Value:Size/TypeSpecifierList

概括如下:

  1. 一对双尖括号表示一串二进制,元素之间用逗号分隔:<<E1,...,En>>

  2. 每个元素完整语法是这样的:Value:Size/TypeSpecifierList,其中 Size 和 TypeSpecifierList 均可忽略

  3. Size 取值范围 1-255,默认值为 8

  4. TypeSpecifierList 是类型说明列表,用中划线 - 分隔,每一项可以是 Type | Signedness | Endianness | Unit

  5. Type = integer | float | binary | bytes | bitstring | bits | utf8 | utf16 | utf32

  6. Signedness = signed | unsigned

  7. Endianness = big | little | native

  8. Unit = unit:IntegerLiteral。Unit 表示 Size 的单位,integer、float、bitstring 类型默认为 1,binary 类型默认为 8

二进制操作

通过模式串创建出bistring对象后,可以用该对象来进行二进制编解码的操作,核心操作为 pack 和 unpack,分别用来打包和解包二进制。

  1. Pattern:pack(params)
  2. Pattern:unpack(data, ret_some, raw_binary)

由于集成了二进制匹配语法,现在可以将协议常量编码到格式串中了,下面的例子展示了如何使用bitstring。

lua
local bs = require 'mc.bitstring'
local s_pack = string.pack

local function b_unpack(pattn, data)
  local p = bs.new(pattn)
  return p:unpack(data)
end

function TestBitString:test_unpack_integer_endiannes()
  local i8, i16, i32, i64 = 0x12, 0x1234, 0x12345678, 0x1234567800ABCDEF
  local data = s_pack('<bhi4i8', i8, i16, i32, i64)
  local r = b_unpack('<<var1:1/integer-unit:8, var2:2/big-unit:8, var3:4/little-unit:8, var4:8/big-unit:8>>', data)
  lu.assertEquals(r.var1, 0x12)
  lu.assertEquals(r.var2, 0x3412)
  lu.assertEquals(r.var3, 0x12345678)
  lu.assertEquals(r.var4, 0xEFCDAB0078563412)
end

IPMI库

IPMI是什么

IPMI是智能型平台管理接口(Intelligent Platform Management Interface)的缩写,是管理基于 Intel结构的企业系统中所使用的外围设备采用的一种工业标准,该标准由英特尔、惠普、NEC、美国戴尔电脑和SuperMicro等公司制定。用户可以利用IPMI监视服务器的物理健康特征,如温度、电压、风扇工作状态、电源状态等。

ipmi命令发送

除了注册ipmi命令回调外,openUBMC还提供了发送IPMI命令的能力,用于和IMU沟通,IMU可以在带内访问CPU、内存、PCIe等信息,并提供给BMC,用于带外管理。

ipmi发送能力由ipmi_core组件提供,并通过require 'ipmi'引入,使用示例如下:

lua
local ipmi = require 'ipmi'
local bs = require 'mc.bitstring'
local enums = require 'ipmi.enums'

local bs_req = bs.new([[<<
    param_a:48,
    param_b:4,
    param_c:4>>]])

local channel_type = enums.ChannelType
local comp_code = ipmi.types.Cc

def get_info_with_ipmi(bus, a, b, c)
    local req_data = bs_req:pack({
        param_a = a,
        param_b = b,
        param_c = c,
    })
    local result, payload
    local info_arr
    for _ = 1, 3 do
        result, payload = ipmi.request(bus, channel_type.CT_ME:value(),
            {DestNetFn = <改为实际NetFn>, Cmd = <改为实际Cmd>, Payload = req_data})
        if result == comp_code.Success then
            return info_arr
        end
    end
end

利用前述的bitstring可以进行编解码,将编码后的结果作为ipmi.request()函数的一部分参数传入,同时在参数中指定实际的NetFn和Cmd,即可实现ipmi命令发送。由于该函数通过调用资源协作接口实现,因此需要传入D-Bus示例

错误引擎

错误定义

  1. 明确错误的边界定义,特别是要与Event做区分,具体定义:

    • bmc处理业务时不能按正常业务流程执行成功的,称为错误

    • bmc正常运行过程中检测到的业务异常,称为Event

  2. 错误是面向bmc系统的,组件对外的错误需要统一管控

  3. 错误按照Redfish标准来定义(全量继承Redfish已定义的错误),需要确保错误的唯一性

  4. 错误统一放到mdb_interface管理

    • 在messages目录下对错误进行定义,可按需新增目录和文件
    • 构建时生成代码集中部署到/opt/bmc/lualib/messsages目录

如何定义错误

在mdb_interface/messages/base.json中定义Redfish标准错误;在mdb_interface/messages/custom.json中定义openUBMC自定义错误。添加错误在Messages字段内添加。

如果在代码中抛出了未定义的错误,会统一转换成InternalError返回给用户。

定义格式为:

json
"ErrorName": {
    "Description": "error description",
    "Message": "error message,args %1,%2",
    "Severity": "Warning",
    "NumberOfArgs": 2,
    "ArgTypes": [
        "string",
        "string"
    ],
    "Resolution": "sth to fix",
    "HttpStatusCode": 400,
    "IpmiCompletionCode": "0xFF"
}

字段描述:

  • Description:消息描述

  • Message:消息内容,如果有args则填充到内容中

  • Severity:严重程度

  • NumberOfArgs:参数数量

  • ArgTypes:参数类型表,如果参数数量为0 不存在该字段

  • Resolution:解决措施

  • HttpStatusCode:http协议返回码

  • IpmiCompletionCode:ipmi协议返回码

如何抛出错误

首先 require 错误引擎模块

lua
local base_messages = require 'messages.base'

然后通过该模块抛出配置好的错误,参数中填入参数列表中的内容

lua
error(base_messages.ErrorName("Arg1", "Arg2"))