一、背景知识
简介
IPMI(Intelligent Platform Management Interface)即智能平台管理接口是使硬件管理具备“智能化”的新一代通用接口标准。用户可以利用 IPMI 监视服务器的物理特征,如温度、电压、电扇工作状态、电源供应以及机箱入侵等。Ipmi 最大的优势在于它是独立于 CPU BIOS 和 OS 的,所以用户无论在开机还是关机的状态下,只要接通电源就可以实现对服务器的监控。Ipmi 是一种规范的标准,其中最重要的物理部件就是BMC(Baseboard Management Controller 如图1),一种嵌入式管理微控制器,它相当于整个平台管理的“大脑”,通过它 ipmi 可以监控各个传感器的数据并记录各种事件的日志。
ipmitool 是一种可用在 linux 系统下的命令行方式的 ipmi 平台管理工具,它支持 ipmi 1.5 规范(最新的规范为 ipmi 2.0),通过它可以实现获取传感器的信息、显示系统日志内容、网络远程开关机等功能。Ipmitool 有两种使用方式。
方式 | 接口 | 特点 |
---|---|---|
本地调用 | 本地操作系统 | 通过OS内核提供接口,与BMC通信(图IPMI中的①方式) |
远程调用 | 网络 | 通过网络以UDP报文的形式与BMC通信(图IPMI中的②方式) |
核心功能:
- 系统日志内容获取
- 服务器温度、电压、风扇状态(亲测)
- 服务器电源状态查询、上电、下电、重启(亲测)
使用 ipmi 的先决条件
想要实现对服务器的 ipmi 管理,必须在硬件、OS、管理工具等几个方面都满足:
服务器硬件本身提供对 ipmi 的支持
目前惠普、戴尔和 NEC 等大多数厂商的服务器都支持 IPMI 1.5,但并不是所有服务器都支持,所以应该先通过产品手册或在 BIOS 中确定服务器是否支持 ipmi,也就是说服务器在主板上要具有 BMC 等嵌入式的管理微控制器。
操作系统提供相应的 ipmi 驱动
通过操作系统监控服务器自身的 ipmi 信息时需要系统内核提供相应的支持,linux 系统通过内核对 OpenIPMI(ipmi 驱动)的支持来提供对 ipmi 的系统接口。
ipmi 管理工具
本文选择的是 Linux 下的命令行方式的 ipmi 平台管理工具 ipmitool。
实战
在监控客户端安装 ipmitool,并远程操作服务器电源
- 依次执行如下命令,安装 ipmitool
tar zxvf impitool-1.5.9.tar.gz
cd impitool-1.5.9
./configure -enable-intf-open=static # 注意:./configure配置lan接口
make
sudo make install
- 通过 ipmitool 命令获取 cpu 温度
ipmitool -I open sensor get "Processor 1 Temp"
Locating sensor record
Sensor ID :Processor 1 Temp(0x32)
Sensor Type(analog):Temperatyre
Sensor Reading:28(+/-6) degree C
Status :ok
Lower Critical:5.000
Lower Non-Critical:10.000
Upper Non-Critical:68.000
Upper Critical:73.000
Uppper Non-Recoverable:na
- 通过 ipmitool 命令远程控制服务器电源
# 显示被监控服务器的电源状态
impitool -I lan -H 10.151.10.20 -P 22 chassis power status
Chassis Power is on
# 远程关机
impiltool -I lan -H 10.151.10.20 -P 22 chassis power off
# 显示被监控服务器的电源状态
impitool -I lan -H 10.151.10.20 -P 22 chassis power status
Chassis Power is off
# 远程开机
impiltool -I lan -H 10.151.10.20 -P 22 chassis power on
Chassis Power is Up/On
# 显示被监控服务器的电源状态
impitool -I lan -H 10.151.10.20 -P 22 chassis power status
Chassis Power is on
注意:通过 ipmitool 还可以监控风扇、机箱等众多相关信息,具体的使用方式见 ipmitool manpage
二、需求描述
需要通过IPMI协议与BMC进行通信,完成电源状态信息的查询、以及上线电的操作。
网上查询资料了解到有一个xmIPMI的包,但是使用者比较少,参考资料较少,因此只能学习官网提供的Demo实现需要的功能。
此外,vmIPMI没有在中央maven仓库中,且公司的maven仓库没有上传包的权限,因此需要将该第三包加入到项目中来。
参考了参考资料3中方案2的建议,将下载到的源码编译后作为工程的一个子模块,完美地解决了上述问题,其符合软件设计中的高内聚、低耦合的原则,具有一定的扩展性。
三、需求实现
构建子模块
打开idea,选中最外层的项目,右键选择“New”->"Module"
点击“Next”按钮,参考源码包中的提供的pom文件,groupId填写fr.jrds.verax
,ArtifactId填写vxIPMI
,version填写1.0.17.2-SNAPSHOT
。将vmIPMI的工程路径设置在最最层项目中,完成后点击确定。
此时,在父pom文件中,会自动生成如下的信息:
<modules>
<module>vxIPMI</module>
</modules>
接下来去完善一下vmIMPI的pom文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>xxx</artifactId>
<groupId>xxx</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>fr.jrds.verax</groupId>
<artifactId>vxIPMI</artifactId>
<name>Verax IPMI module</name>
<version>1.0.17.2-SNAPSHOT</version>
<packaging>jar</packaging>
<build>
<finalName>vxIPMI</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.2</version>
<configuration>
<source>${buildSource}</source>
<target>${buildTarget}</target>
<showWarnings>true</showWarnings>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.9</version>
<configuration>
<skipTests>true</skipTests>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<reporting>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>2.10.1</version>
<configuration>
<links>
<link>https://docs.oracle.com/javaee/6/api/</link>
<link>http://junit.org/apidocs/</link>
<link>http://logging.apache.org/log4j/1.2/apidocs/</link>
</links>
</configuration>
</plugin>
</plugins>
</reporting>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.14</version>
<scope>compile</scope>
</dependency>
</dependencies>
<properties>
<project.build.mainClass>com.veraxsystems.vxipmi.encoding.DecoderRunner</project.build.mainClass>
<junit.version>4.0</junit.version>
<!-- The project should have a source encoding set. -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<buildSource>1.7</buildSource>
<buildTarget>1.7</buildTarget>
</properties>
</project>
然后把源码包中的源代码复制到新建的vmIPMI工程相应的目录下。
最后,在需要使用到vmIPMI的pom文件中加入如下的信息,表明模块间的依赖关系:
<dependency>
<groupId>fr.jrds.verax</groupId>
<artifactId>vxIPMI</artifactId>
<version>1.0.17.2-SNAPSHOT</version>
</dependency>
最后,整体通过mvn test-compile
进行编译,测试一下。
本人是一次就顺利通过编译测试了。
示例代码
官网参考代码
/*
* VxipmiRunner.java
* Created on 2011-09-20
*
* Copyright (c) Verax Systems 2011.
* All rights reserved.
*
* This software is furnished under a license. Use, duplication,
* disclosure and all other uses are restricted to the rights
* specified in the written license agreement.
*/
package com.veraxsystems.vxipmi.test;
import com.veraxsystems.vxipmi.api.async.ConnectionHandle;
import com.veraxsystems.vxipmi.api.sync.IpmiConnector;
import com.veraxsystems.vxipmi.coding.commands.IpmiVersion;
import com.veraxsystems.vxipmi.coding.commands.PrivilegeLevel;
import com.veraxsystems.vxipmi.coding.commands.chassis.ChassisControl;
import com.veraxsystems.vxipmi.coding.commands.chassis.GetChassisStatus;
import com.veraxsystems.vxipmi.coding.commands.chassis.GetChassisStatusResponseData;
import com.veraxsystems.vxipmi.coding.commands.chassis.PowerCommand;
import com.veraxsystems.vxipmi.coding.commands.session.SetSessionPrivilegeLevel;
import com.veraxsystems.vxipmi.coding.protocol.AuthenticationType;
import com.veraxsystems.vxipmi.coding.security.CipherSuite;
import java.net.InetAddress;
public class ChassisControlRunner {
/**
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception {
IpmiConnector connector;
// Create the connector, specify port that will be used to communicate
// with the remote host. The UDP layer starts listening at this port, so
// no 2 connectors can work at the same time on the same port.
connector = new IpmiConnector(6000);
System.out.println("Connector created");
// Create the connection and get the handle, specify IP address of the
// remote host. The connection is being registered in ConnectionManager,
// the handle will be needed to identify it among other connections
// (target IP address isn't enough, since we can handle multiple
// connections to the same host)
ConnectionHandle handle = connector.createConnection(InetAddress.getByName("192.168.1.1"));
System.out.println("Connection created");
// Get available cipher suites list via getAvailableCipherSuites and
// pick one of them that will be used further in the session.
CipherSuite cs = connector.getAvailableCipherSuites(handle).get(3);
System.out.println("Cipher suite picked");
// Provide chosen cipher suite and privilege level to the remote host.
// From now on, your connection handle will contain these information.
connector.getChannelAuthenticationCapabilities(handle, cs, PrivilegeLevel.Administrator);
System.out.println("Channel authentication capabilities receivied");
// Start the session, provide username and password, and optionally the
// BMC key (only if the remote host has two-key authentication enabled,
// otherwise this parameter should be null)
connector.openSession(handle, "user", "pass", null);
System.out.println("Session open");
// Send some message and read the response
GetChassisStatusResponseData rd = (GetChassisStatusResponseData) connector.sendMessage(handle,
new GetChassisStatus(IpmiVersion.V20, cs, AuthenticationType.RMCPPlus));
System.out.println("Received answer");
System.out.println("System power state is " + (rd.isPowerOn() ? "up" : "down"));
// Set session privilege level to administrator, as ChassisControl command requires this level
connector.sendMessage(handle, new SetSessionPrivilegeLevel(IpmiVersion.V20, cs, AuthenticationType.RMCPPlus,
PrivilegeLevel.Administrator));
ChassisControl chassisControl = null;
//Power on or off
if (!rd.isPowerOn()) {
chassisControl = new ChassisControl(IpmiVersion.V20, cs, AuthenticationType.RMCPPlus, PowerCommand.PowerUp);
} else {
chassisControl = new ChassisControl(IpmiVersion.V20, cs, AuthenticationType.RMCPPlus,
PowerCommand.PowerDown);
}
connector.sendMessage(handle, chassisControl);
// Close the session
connector.closeSession(handle);
System.out.println("Session closed");
// Close connection manager and release the listener port.
connector.tearDown();
System.out.println("Connection manager closed");
}
}
中文注释版的参考代码
机器的BMC信息通常比较固定,初始化之后一般不会变化,可以持久化存储下来,建表语句如下:
CREATE TABLE `server_bmc` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`create_time` timestamp NULL DEFAULT NULL,
`modify_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`server_id` int(11) NOT NULL,
`bmc_ip` varchar(50) NOT NULL,
`bmc_port` int(8) NOT NULL DEFAULT '22',
`bmc_username` varchar(255) NOT NULL DEFAULT '',
`bmc_password` varchar(255) NOT NULL DEFAULT '',
`bmc_key` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `server_id_key` (`server_id`) USING BTREE,
KEY `bmc_ip_key` (`bmc_ip`),
CONSTRAINT `server_bmc_fk` FOREIGN KEY (`server_id`) REFERENCES `server` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=42 DEFAULT CHARSET=utf8;
实际的业务代码:
import com.veraxsystems.vxipmi.api.async.ConnectionHandle;
import com.veraxsystems.vxipmi.api.sync.IpmiConnector;
import com.veraxsystems.vxipmi.coding.commands.IpmiVersion;
import com.veraxsystems.vxipmi.coding.commands.PrivilegeLevel;
import com.veraxsystems.vxipmi.coding.commands.chassis.*;
import com.veraxsystems.vxipmi.coding.commands.session.SetSessionPrivilegeLevel;
import com.veraxsystems.vxipmi.coding.protocol.AuthenticationType;
import com.veraxsystems.vxipmi.coding.security.CipherSuite;
import org.junit.Test;
import java.net.InetAddress;
import java.util.List;
/**
* 带外操作功能基础测试
*/
public class ChassisControlTest {
private static final String TEST_IP = "10.246.176.140";
private static final Integer TEST_PORT = 22;
private static final String TEST_USERNAME = "root";
private static final String TEST_PASSWORD = "calvin";
@Test
public void testChassisControll() {
IpmiConnector connector = null;
ConnectionHandle handle = null;
try {
/*
* 创建连接器,指定用于与远程主机之间进行通信的端口。UDP层开始在该端口上监听,因此
* 不允许有两个连接器在同一时间工作在同一个端口上。
*/
connector = new IpmiConnector(TEST_PORT);
System.out.println("Connector created");
/*
* 创建连接并获取句柄,需要指定远程主机的IP地址。连接正在ConnectionManager中注册,需要句柄在其他连接中标识它
* (目标IP地址还不够,因为我们可以处理到同一主机的多个连接)
*/
handle = connector.createConnection(InetAddress.getByName(TEST_IP));
System.out.println("Connection created");
// 通过getAvailableCipherSuites()方法获取可用的密码套件列表, 并选择其中的一个将在会话中进一步使用
CipherSuite cs = connector.getAvailableCipherSuites(handle).get(3);
System.out.println("Cipher suite picked");
/*
* 向远程主机提供选定的密码套件和特权级别。从现在起,连接句柄将包含这些信息。
*/
connector.getChannelAuthenticationCapabilities(handle, cs, PrivilegeLevel.Administrator);
System.out.println("Channel authentication capabilities receivied");
// 开始会话,提供用户名和密码,以及可选的BMC key(当且仅当远程主机拥有双key校验时使用),否则这一参数填写null即可。
connector.openSession(handle, TEST_USERNAME, TEST_PASSWORD, null);
System.out.println("Session open");
// 发送一些信息并且读取响应
GetChassisStatusResponseData rd = (GetChassisStatusResponseData) connector.sendMessage(handle,
new GetChassisStatus(IpmiVersion.V20, cs, AuthenticationType.RMCPPlus));
System.out.println("Received answer");
System.out.println("System power state is " + (rd.isPowerOn() ? "up" : "down"));
// 设置会话权限级别为管理员,因为底层控制指令需要用到这一级别
connector.sendMessage(handle, new SetSessionPrivilegeLevel(IpmiVersion.V20, cs, AuthenticationType.RMCPPlus,
PrivilegeLevel.Administrator));
ChassisControl chassisControl = null;
// 上电或者下电
if (!rd.isPowerOn()) {
chassisControl = new ChassisControl(IpmiVersion.V20, cs, AuthenticationType.RMCPPlus, PowerCommand.PowerUp);
} else {
chassisControl = new ChassisControl(IpmiVersion.V20, cs, AuthenticationType.RMCPPlus, PowerCommand.PowerDown);
}
// 发送上下电请求消息
connector.sendMessage(handle, chassisControl);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (connector != null) {
// 关闭会话
try {
connector.closeSession(handle);
System.out.println("Session closed");
} catch (Exception e) {
e.printStackTrace();
}
// 关闭连接管理器并且释放监听端口
connector.tearDown();
System.out.println("Connection manager closed");
}
}
}
}
封装代码
import com.netease.cloud.cm.pri.common.exception.CMDBRuntimeException;
import com.veraxsystems.vxipmi.api.async.ConnectionHandle;
import com.veraxsystems.vxipmi.api.sync.IpmiConnector;
import com.veraxsystems.vxipmi.coding.commands.IpmiVersion;
import com.veraxsystems.vxipmi.coding.commands.PrivilegeLevel;
import com.veraxsystems.vxipmi.coding.commands.chassis.*;
import com.veraxsystems.vxipmi.coding.commands.session.SetSessionPrivilegeLevel;
import com.veraxsystems.vxipmi.coding.payload.lan.IPMIException;
import com.veraxsystems.vxipmi.coding.protocol.AuthenticationType;
import com.veraxsystems.vxipmi.coding.security.CipherSuite;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.net.InetAddress;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 带外操作服务实现类
*/
@Service
public class OutOfBandService {
private static final Logger logger = LoggerFactory.getLogger(OutOfBandService.class);
private static final Map<Integer, String> messageMap = new HashMap<>();
static {
messageMap.put(0, "执行成功");
messageMap.put(1, "开机状态");
messageMap.put(2, "关机状态");
messageMap.put(520, "该角色没有接口权限");
messageMap.put(522, "缺少请求参数");
messageMap.put(550, "user或password参数错误");
messageMap.put(560, "没有该SN记录");
messageMap.put(570, "action参数错误");
messageMap.put(575, "当前状态不支持此操作");
messageMap.put(580, "没有操作服务器的权限");
messageMap.put(585, "请求SN与实际SN不匹配");
messageMap.put(590, "无法连接远程管理IP");
messageMap.put(595, "连接超时");
}
public String getMessage(int code) {
return messageMap.get(code);
}
public int action(String ip, Integer port, String username, String password, String action) {
return action(ip, port, username, password, action, null);
}
/**
* @param ip
* @param port
* @param username
* @param password
* @param action 指定动作
* @param bmcKey 二次校验密码
* @return
*/
public int action(String ip, Integer port, String username, String password, String action, String bmcKey) {
IpmiConnector connector = null;
ConnectionHandle handle = null;
try {
// 校验参数
int code;
if (StringUtils.isEmpty(ip) || port == null || StringUtils.isEmpty(username)
|| StringUtils.isEmpty(password) || StringUtils.isEmpty(action)) {
code = 522;
logger.error(this.getMessage(code));
return code;
}
// 连接器
connector = new IpmiConnector(port);
// 创建连接并获取句柄,需要指定远程主机的IP地址
handle = connector.createConnection(InetAddress.getByName(ip));
// 通过getAvailableCipherSuites()方法获取可用的密码套件列表, 并选择其中的一个将在会话中进一步使用
List<CipherSuite> suites = connector.getAvailableCipherSuites(handle);
// 密码套件
CipherSuite cs;
if (suites.size() > 3) {
cs = suites.get(3);
} else if (suites.size() > 2) {
cs = suites.get(2);
} else if (suites.size() > 1) {
cs = suites.get(1);
} else {
cs = suites.get(0);
}
logger.debug("Cipher suite picked");
// 选择密码套件并请求会话特权级别
connector.getChannelAuthenticationCapabilities(handle, cs, PrivilegeLevel.Administrator);
logger.debug("Channel authentication capabilities receivied");
// 打开会话并验证
if (bmcKey != null) {
connector.openSession(handle, username, password, bmcKey.getBytes());
} else {
connector.openSession(handle, username, password, null);
}
logger.debug("Session open");
PowerCommand powerCommand;
switch (action) {
case "powerstatus"://查询电源状态
GetChassisStatusResponseData rd = (GetChassisStatusResponseData) connector.sendMessage(handle,
new GetChassisStatus(IpmiVersion.V20, cs, AuthenticationType.RMCPPlus));
if (rd.isPowerOn()) {
code = 1;//开机状态
} else {
code = 2;//关机状态
}
break;
case "poweron"://上电
powerCommand = PowerCommand.PowerUp;
code = sendCommand(connector, handle, cs, powerCommand);
break;
case "poweroff"://下电
powerCommand = PowerCommand.PowerDown;
code = sendCommand(connector, handle, cs, powerCommand);
break;
case "hardreset"://重启
powerCommand = PowerCommand.HardReset;
code = sendCommand(connector, handle, cs, powerCommand);
break;
default:
code = 570;
return code;
}
return code;
} catch (IOException e) {
logger.error("Get connection timeout");
return 595;
} catch (Exception e) {
logger.error("带外操作失败:" + e.getMessage());
return 590;
} finally {
if (connector != null) {
if (handle != null) {
// 关闭会话
try {
connector.closeSession(handle);
logger.debug("Session closed");
} catch (Exception e) {
e.printStackTrace();
}
}
// 关闭连接管理器并且释放监听端口
connector.tearDown();
logger.debug("Connection manager closed");
}
}
}
private int sendCommand(IpmiConnector connector, ConnectionHandle handle, CipherSuite cs, PowerCommand powerCommand) {
try {
// 设置会话权限级别为管理员,因为底层控制指令需要用到这一级别
connector.sendMessage(handle, new SetSessionPrivilegeLevel(IpmiVersion.V20, cs, AuthenticationType.RMCPPlus,
PrivilegeLevel.Administrator));
// 设置控制指令信息
ChassisControl chassisControl = new ChassisControl(IpmiVersion.V20, cs, AuthenticationType.RMCPPlus, powerCommand);
// 发送指令请求执行
connector.sendMessage(handle, chassisControl);
// 返回成功执行的结果
return 0;
} catch (IPMIException e1) {
logger.error(e1.getMessage());
return 575;
} catch (Exception e2) {
String msg = "上下电命令操作失败:" + e2.getMessage();
logger.error(msg);
throw new CMDBRuntimeException(msg);
}
}
}