之前有详细讲过,服务器实现IPMI智能管理需要从硬件和软件层面提供支持,硬件需要嵌入式微控制器BMC,软件上需要内核提供ipmi驱动以及用户层与BMC交互的管理工具,今天就聊聊与BMC交互的工具ipmitool。服务器管理员可以通过ipmitool从BMC那边获取服务器的一些运行状况。这样的管理工具不仅仅只有ipmitool这一款,比如freeipmi以及各家根据标准ipmi编写的工具,这些工具功能上大致一样,ipmitool应该是目前使用最广最好用的工具。现在RedFish API开始慢慢接替ipmitool,一般BMC都会同时支持ipmitool命令行和RedFish API。
ipmitool是一个开源软件,所以它的安装包一般就放有源码,可以从网下免费下载,这里提供其中一个下载链接https://zh.osdn.net/projects/sfnet_ipmitool/downloads/ipmitool/1.8.18/ipmitool-1.8.18.tar.bz2/,以最新的Linux版ipitool-1.8.18为例,梳理一下ipmitool 一次收发raw data的过程。
首先,看看ipmitool这个软件安装包的目录结构,看起来一大堆,我们只需要关注其中3个文件夹就好:include/ipmitool、lib、src,其他都是辅助文档和安装所需的文件,都可以忽略不看。include/ipmitool目录下存放着ipmitool源码的头文件,这些头文件是lib目录里面的C文件需要引用的,基本一一对应,src是整个程序的入口,src/plugins里面包含了ipmi各个接口的定义(比如lan、lanplus、open、serial等),ipmitool将这些接口当做插件的形式调用。从大框架上一看,ipmitool的源码结构是不是就简单到一目了然了呢。
include目录都是些头文件,也不需要特别关注,在追溯源码流程涉及到函数、结构体等具体声明定义的时候到对应头文件查看即可,所以真正需要倾注注意力去看的只剩下lib和src里面的内容了。
先来看看lib目录,ipmi的核心源码都在这儿了,不多,也就38个.c文件,每个C文件都对应BMC的某项功能,比如sdr、sel、sensor、fru等,这几个是常用的不能再常用的内容了。这次只会用到ipmi_raw.c
再看看src,第一层目录只需要看ipmitool.c文件,其他不看,第二层目录plugins关注open目录和ipmi_intf.c就行,带内的raw data的发送和接收调用的是open这个接口,其他目录对应的其他接口,不用看。
好了,讲这么多就是为了简化代码,减少看到这么多文件时的压力,下面进入正题。
1、基本调用关系
以下是ipmitool raw data发送过程的函数调用的流程图,ipmitool.c中的main()函数是整个程序的入口,main函数中调用ipmi_main(),然后一级一级调用。
2、ipmitool.c
这里主要涉及到ipmi_cmd这个结构体,定义出自ipmi_intf.h。第一个成员是一个指向函数的指针,在C里称为函数指针,最好先把概念理解清楚,不然后面会看不懂。
//ipmi_intf.h
struct ipmi_cmd {
int (*func)(struct ipmi_intf * intf, int argc, char ** argv);
const char * name;
const char * desc;
};
ipmitool.c首先就创建并初始化了一个数组,数组里面的每个元素都是一个ipmi_cmd的结构体实例。
//ipmitool.c
struct ipmi_cmd ipmitool_cmd_list[] = {
{ ipmi_raw_main, "raw", "Send a RAW IPMI request and print response" },
{ ipmi_rawi2c_main, "i2c", "Send an I2C Master Write-Read command and print response" },
{ ipmi_rawspd_main, "spd", "Print SPD info from remote I2C device" },
{ ipmi_lanp_main, "lan", "Configure LAN Channels" },
{ ipmi_chassis_main, "chassis", "Get chassis status and set power state" },
{ ipmi_power_main, "power", "Shortcut to chassis power commands" },
{ ipmi_event_main, "event", "Send pre-defined events to MC" },
{ ipmi_mc_main, "mc", "Management Controller status and global enables" },
{ ipmi_mc_main, "bmc", NULL }, /* for backwards compatibility */
{ ipmi_sdr_main, "sdr", "Print Sensor Data Repository entries and readings" },
{ ipmi_sensor_main, "sensor", "Print detailed sensor information" },
{ ipmi_fru_main, "fru", "Print built-in FRU and scan SDR for FRU locators" },
{ ipmi_gendev_main, "gendev", "Read/Write Device associated with Generic Device locators sdr" },
{ ipmi_sel_main, "sel", "Print System Event Log (SEL)" },
{ ipmi_pef_main, "pef", "Configure Platform Event Filtering (PEF)" },
{ ipmi_sol_main, "sol", "Configure and connect IPMIv2.0 Serial-over-LAN" },
{ ipmi_tsol_main, "tsol", "Configure and connect with Tyan IPMIv1.5 Serial-over-LAN" },
{ ipmi_isol_main, "isol", "Configure IPMIv1.5 Serial-over-LAN" },
{ ipmi_user_main, "user", "Configure Management Controller users" },
{ ipmi_channel_main, "channel", "Configure Management Controller channels" },
{ ipmi_session_main, "session", "Print session information" },
{ ipmi_dcmi_main, "dcmi", "Data Center Management Interface"},
{ ipmi_nm_main, "nm", "Node Manager Interface"},
{ ipmi_sunoem_main, "sunoem", "OEM Commands for Sun servers" },
{ ipmi_kontronoem_main, "kontronoem", "OEM Commands for Kontron devices"},
{ ipmi_picmg_main, "picmg", "Run a PICMG/ATCA extended cmd"},
{ ipmi_fwum_main, "fwum", "Update IPMC using Kontron OEM Firmware Update Manager" },
{ ipmi_firewall_main,"firewall","Configure Firmware Firewall" },
{ ipmi_delloem_main, "delloem", "OEM Commands for Dell systems" },
#ifdef HAVE_READLINE
{ ipmi_shell_main, "shell", "Launch interactive IPMI shell" },
#endif
{ ipmi_exec_main, "exec", "Run list of commands from file" },
{ ipmi_set_main, "set", "Set runtime variable for shell and exec" },
{ ipmi_echo_main, "echo", NULL }, /* for echoing lines to stdout in scripts */
{ ipmi_hpmfwupg_main,"hpm", "Update HPM components using PICMG HPM.1 file"},
{ ipmi_ekanalyzer_main,"ekanalyzer", "run FRU-Ekeying analyzer using FRU files"},
{ ipmi_ime_main, "ime", "Update Intel Manageability Engine Firmware"},
{ ipmi_vita_main, "vita", "Run a VITA 46.11 extended cmd"},
{ ipmi_lan6_main, "lan6", "Configure IPv6 LAN Channels"},
{ NULL },
};
这里有个关于C语言的语法知识,数组的名字就是指向数组第一个元素的首地址,所以也可以认为数组的名字是指向这个数组的指针。这里将ipmitool_cmd_list就是ipmitool_cmd_list[]数组的指针。
//ipmitool.c
int
main(int argc, char ** argv)
{
int rc;
rc = ipmi_main(argc, argv, ipmitool_cmd_list, NULL); //ipmitool_cmd_list就是指向ipmi_cmd_list[]数组的指针
if (rc < 0)
exit(EXIT_FAILURE);
else
exit(EXIT_SUCCESS);
}
3、ipmitool_mian.c
跳到ipmi_main()函数的定义,这里涉及到接口的调用,ipmi_intf_load()函数就是用来加载用户会调用哪个接口,这个是根据命令行中-I 后面的值决定的,带内可以省略这个参数,默认就是open接口。
//ipmitool_main.c
/* load interface */
ipmi_main_intf = ipmi_intf_load(intfname);
if (ipmi_main_intf == NULL) {
lprintf(LOG_ERR, "Error loading interface %s", intfname);
goto out_free;
}
4、ipmi_intf.c
ipmi_intf_table[]这个数组就是存放各个接口的。最后指向ipmi_intf_table[0]。
//ipmi_intf.c
struct ipmi_intf * ipmi_intf_load(char * name)
{
struct ipmi_intf ** intf;
struct ipmi_intf * i;
if (name == NULL) {
i = ipmi_intf_table[0]; //这里表示如果没有-I和后面的参数,默认取ipmi_intf_table[0]
if (i->setup != NULL && (i->setup(i) < 0)) {
lprintf(LOG_ERR, "Unable to setup "
"interface %s", name);
return NULL;
}
return i;
}
for (intf = ipmi_intf_table;
((intf != NULL) && (*intf != NULL));
intf++) {
i = *intf;
if (strncmp(name, i->name, strlen(name)) == 0) {
if (i->setup != NULL && (i->setup(i) < 0)) {
lprintf(LOG_ERR, "Unable to setup "
"interface %s", name);
return NULL;
}
return i;
}
}
return NULL;
}
ipmi_intf_table[0]就是&ipmi_open_intf,是一个地址,这个地址指向了open.c中的ipmi_open_intf结构体。
//ipmi_intf.c
struct ipmi_intf * ipmi_intf_table[] = {
#ifdef IPMI_INTF_OPEN
&ipmi_open_intf,
#endif
#ifdef IPMI_INTF_IMB
&ipmi_imb_intf,
#endif
#ifdef IPMI_INTF_LIPMI
&ipmi_lipmi_intf,
#endif
#ifdef IPMI_INTF_BMC
&ipmi_bmc_intf,
#endif
#ifdef IPMI_INTF_LAN
&ipmi_lan_intf,
#endif
#ifdef IPMI_INTF_LANPLUS
&ipmi_lanplus_intf,
#endif
#ifdef IPMI_INTF_FREE
&ipmi_free_intf,
#endif
#ifdef IPMI_INTF_SERIAL
&ipmi_serial_term_intf,
&ipmi_serial_bm_intf,
#endif
#ifdef IPMI_INTF_DUMMY
&ipmi_dummy_intf,
#endif
#ifdef IPMI_INTF_USB
&ipmi_usb_intf,
#endif
NULL
};
5、open.c
ipmi_open_intf是ipmi_intf结构体的一个实例,下面是ipmi_open_intf的部分初始化,基本都是函数指针,指定了调用open接口会用到的一些函数。
//open.c
struct ipmi_intf ipmi_open_intf = {
.name = "open",
.desc = "Linux OpenIPMI Interface",
.setup = ipmi_openipmi_setup,
.open = ipmi_openipmi_open,
.close = ipmi_openipmi_close,
.sendrecv = ipmi_openipmi_send_cmd,
.set_my_addr = ipmi_openipmi_set_my_addr,
.my_addr = IPMI_BMC_SLAVE_ADDR,
.target_addr = 0, /* init so -m local_addr does not cause bridging */
};
ipmi_intf结构体如下,成员众多,上面的实例ipmi_open_intf只为其成员赋值了一部分。
//ipmi_intf.h
struct ipmi_intf {
char name[16];
char desc[128];
char *devfile;
int fd;
int opened;
int abort;
int noanswer;
int picmg_avail;
int vita_avail;
IPMI_OEM manufacturer_id;
int ai_family;
struct ipmi_session_params ssn_params;
struct ipmi_session * session;
struct ipmi_oem_handle * oem;
struct ipmi_cmd * cmdlist;
uint8_t target_ipmb_addr;
uint32_t my_addr;
uint32_t target_addr;
uint8_t target_lun;
uint8_t target_channel;
uint32_t transit_addr;
uint8_t transit_channel;
uint16_t max_request_data_size;
uint16_t max_response_data_size;
uint8_t devnum;
int (*setup)(struct ipmi_intf * intf);
int (*open)(struct ipmi_intf * intf);
void (*close)(struct ipmi_intf * intf);
struct ipmi_rs *(*sendrecv)(struct ipmi_intf * intf, struct ipmi_rq * req);
int (*sendrsp)(struct ipmi_intf * intf, struct ipmi_rs * rsp);
struct ipmi_rs *(*recv_sol)(struct ipmi_intf * intf);
struct ipmi_rs *(*send_sol)(struct ipmi_intf * intf, struct ipmi_v2_payload * payload);
int (*keepalive)(struct ipmi_intf * intf);
int (*set_my_addr)(struct ipmi_intf * intf, uint8_t addr);
void (*set_max_request_data_size)(struct ipmi_intf * intf, uint16_t size);
void (*set_max_response_data_size)(struct ipmi_intf * intf, uint16_t size);
};
6、ipmi_main.c
加载open接口后,ipmi_main()中调用了ipmi_cmd_run()
//ipmi_mian.c
int
ipmi_cmd_run(struct ipmi_intf * intf, char * name, int argc, char ** argv)
{
struct ipmi_cmd * cmd = intf->cmdlist; //intf->cmdlist指向的就是ipmitool_cmd_list[]数组
/* hook to run a default command if nothing specified */
if (name == NULL) {
if (cmd->func == NULL || cmd->name == NULL)
return -1;
else if (strncmp(cmd->name, "default", 7) == 0)
return cmd->func(intf, 0, NULL);
else {
lprintf(LOG_ERR, "No command provided!");
ipmi_cmd_print(intf->cmdlist);
return -1;
}
}
for (cmd=intf->cmdlist; cmd->func != NULL; cmd++) {
if (strncmp(name, cmd->name, __maxlen(cmd->name, name)) == 0)
break;
}
if (cmd->func == NULL) {
cmd = intf->cmdlist;
if (strncmp(cmd->name, "default", 7) == 0)
return cmd->func(intf, argc+1, argv-1);
lprintf(LOG_ERR, "Invalid command: %s", name);
ipmi_cmd_print(intf->cmdlist);
return -1;
}
return cmd->func(intf, argc, argv);
}
中间的一个for循环很关键,这里遍历的是ipmitool_cmd_list[]这个数组,根据你传入的值(这里是raw),确定调用对应的函数。比如我今天发送的是 raw data,用的命令是ipmitool raw 0x06 0x01,这里的name就是等于raw,根据raw,cmd->func指向的函数名就是ipmi_raw_main,接着跳到这个ipmi_raw_main()这个函数中继续执行。
for (cmd=intf->cmdlist; cmd->func != NULL; cmd++) {
if (strncmp(name, cmd->name, __maxlen(cmd->name, name)) == 0)
break;
}
7、ipmi_raw.c
这里主要弄明白intf->sendrecv指向的是哪个函数就一目了然了,这个在上面已经提到过,ipmi_open_intf中已经赋值过了(.sendrecv = ipmi_openipmi_send_cmd)。正常情况下rsp就是BMC返回的raw data。
//ipmi_raw.c
int
ipmi_raw_main(struct ipmi_intf * intf, int argc, char ** argv)
{
struct ipmi_rs * rsp;
struct ipmi_rq req;
uint8_t netfn, cmd, lun;
uint16_t netfn_tmp = 0;
int i;
uint8_t data[256];
if (argc == 1 && strncmp(argv[0], "help", 4) == 0) {
ipmi_raw_help();
return 0;
}
else if (argc < 2) {
lprintf(LOG_ERR, "Not enough parameters given.");
ipmi_raw_help();
return (-1);
}
else if (argc > sizeof(data))
{
lprintf(LOG_NOTICE, "Raw command input limit (256 bytes) exceeded");
return -1;
}
lun = intf->target_lun;
netfn_tmp = str2val(argv[0], ipmi_netfn_vals);
if (netfn_tmp == 0xff) {
if (is_valid_param(argv[0], &netfn, "netfn") != 0)
return (-1);
} else {
if (netfn_tmp >= UINT8_MAX) {
lprintf(LOG_ERR, "Given netfn \"%s\" is out of range.", argv[0]);
return (-1);
}
netfn = netfn_tmp;
}
if (is_valid_param(argv[1], &cmd, "command") != 0)
return (-1);
memset(data, 0, sizeof(data));
memset(&req, 0, sizeof(req));
req.msg.netfn = netfn;
req.msg.lun = lun;
req.msg.cmd = cmd;
req.msg.data = data;
for (i=2; i<argc; i++) {
uint8_t val = 0;
if (is_valid_param(argv[i], &val, "data") != 0)
return (-1);
req.msg.data[i-2] = val;
req.msg.data_len++;
}
lprintf(LOG_INFO,
"RAW REQ (channel=0x%x netfn=0x%x lun=0x%x cmd=0x%x data_len=%d)",
intf->target_channel & 0x0f, req.msg.netfn,req.msg.lun ,
req.msg.cmd, req.msg.data_len);
printbuf(req.msg.data, req.msg.data_len, "RAW REQUEST");
rsp = intf->sendrecv(intf, &req);
if (rsp == NULL) {
lprintf(LOG_ERR, "Unable to send RAW command "
"(channel=0x%x netfn=0x%x lun=0x%x cmd=0x%x)",
intf->target_channel & 0x0f, req.msg.netfn, req.msg.lun, req.msg.cmd);
return -1;
}
if (rsp->ccode > 0) {
lprintf(LOG_ERR, "Unable to send RAW command "
"(channel=0x%x netfn=0x%x lun=0x%x cmd=0x%x rsp=0x%x): %s",
intf->target_channel & 0x0f, req.msg.netfn, req.msg.lun, req.msg.cmd, rsp->ccode,
val2str(rsp->ccode, completion_code_vals));
return -1;
}
lprintf(LOG_INFO, "RAW RSP (%d bytes)", rsp->data_len);
/* print the raw response buffer */
for (i=0; i<rsp->data_len; i++) {
if (((i%16) == 0) && (i != 0))
printf("\n");
printf(" %2.2x", rsp->data[i]);
}
printf("\n");
return 0;
}
ipmi_openipmi_send_cmd()函数中,调用了ioctrl()实现数据的发送和接收。ioctrl()有点儿类似于linux中的read()和write(),可以说是一种特殊的read()和write()的组合,read()和write()不能实现的读写操作,通过ioctrl()可以实现,这里就不介绍了,没几页纸也写不完,说不透。
//open.c
if (ioctl(intf->fd, IPMICTL_SEND_COMMAND, &_req) < 0) {
lperror(LOG_ERR, "Unable to send command");
if (data != NULL) {
free(data);
data = NULL;
}
return NULL;
}
//open.c
/* get data */
if (ioctl(intf->fd, IPMICTL_RECEIVE_MSG_TRUNC, &recv) < 0) {
lperror(LOG_ERR, "Error receiving message");
if (errno != EMSGSIZE) {
if (data != NULL) {
free(data);
data = NULL;
}
return NULL;
}
}
ipmitool 一次raw data的发送大致过程就是这样,细节可再细看。