之前有详细讲过,服务器实现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的发送大致过程就是这样,细节可再细看。

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐