feat(baremetal): support IPMI RMCP+ cipher suite probe and persist (#25239)

Auto-detect working ipmitool -C suites (0/3/17) and store cipher_suite on host IPMI config for subsequent connections.
This commit is contained in:
Zexi Li
2026-07-28 10:27:20 +08:00
committed by GitHub
parent e7ce74000c
commit 0c04596500
12 changed files with 123 additions and 31 deletions

View File

@@ -1,4 +1,4 @@
FROM registry.cn-beijing.aliyuncs.com/yunionio/baremetal-base:v0.3.9-20251215.0
FROM registry.cn-beijing.aliyuncs.com/yunionio/baremetal-base:v0.3.9-20260724.0
LABEL maintainer="Zexi Li <lizexi@yunionyun.com>"

View File

@@ -1,8 +1,8 @@
#RUN yum install -y https://iso.yunion.cn/vm-images/baremetal-pxerom-1.1.0-21092209.x86_64.rpm
#RUN yum install -y http://192.168.23.50:8083/baremetal-pxerom-1.1.0-21092209.x86_64.rpm
FROM registry.cn-beijing.aliyuncs.com/yunionio/yunionos:v4.0.0-20251201.0 as yunionos
FROM registry.cn-beijing.aliyuncs.com/yunionio/yunionos:v4.0.0-20251203.0 as yunionos
FROM centos:8 as grub-stage
FROM registry.cn-beijing.aliyuncs.com/cloudpods/centos:8 as grub-stage
RUN sed -i 's/mirrorlist/#mirrorlist/g' /etc/yum.repos.d/CentOS-*
RUN sed -i 's|#baseurl=http://mirror.centos.org|baseurl=http://vault.centos.org|g' /etc/yum.repos.d/CentOS-*

View File

@@ -1,6 +1,7 @@
REGISTRY ?= "registry.cn-beijing.aliyuncs.com/yunionio"
DOCKER_BUILD = docker build -t $(REGISTRY)
DOCKER_BUILDX = docker buildx build --platform linux/arm64,linux/amd64,linux/riscv64 --push -t $(REGISTRY)
DOCKER_BUILDX_BM = docker buildx build --platform linux/arm64,linux/amd64 --push -t $(REGISTRY)
debian10-base:
@@ -40,7 +41,7 @@ WEBCONSOLE_BASE_VERSION_3-22-2 = 3.22.2-1
webconsole-base:
$(DOCKER_BUILDX)/webconsole-base:$(WEBCONSOLE_BASE_VERSION_3-22-2) -f ./Dockerfile.webconsole-base .
BAREMETAL_BASE_VERSION = v0.3.9-20251112.1
BAREMETAL_BASE_VERSION = v0.3.9-20260724.0
FEDORA_RISCV64_VERSION = 42
fedora-riscv64-base:
@@ -53,8 +54,9 @@ baremetal-base-riscv: fedora-riscv64-base
$(DOCKER_BUILDX)/baremetal-base:$(BAREMETAL_BASE_VERSION) -f ./Dockerfile.baremetal-base-riscv .
#docker push $(REGISTRY)/baremetal-base:$(BAREMETAL_BASE_VERSION)
# TODO: support riscv64 for baremetal-base
baremetal-base:
$(DOCKER_BUILDX)/baremetal-base:$(BAREMETAL_BASE_VERSION) -f ./Dockerfile.baremetal-base .
$(DOCKER_BUILDX_BM)/baremetal-base:$(BAREMETAL_BASE_VERSION) -f ./Dockerfile.baremetal-base .
#docker push $(REGISTRY)/baremetal-base:$(BAREMETAL_BASE_VERSION)
TORRENT_VERSION = 20210815.0

View File

@@ -81,7 +81,7 @@ func newExecutor(options *BaseOptions) (ipmitool.IPMIExecutor, error) {
if options.Port > 0 {
port = options.Port
}
return ipmitool.NewLanPlusIPMIWithPort(options.HOST, options.User, options.PASSWD, port), nil
return ipmitool.NewLanPlusIPMIWithPort(options.HOST, options.User, options.PASSWD, port)
}
return nil, fmt.Errorf("Unsupported mode: %s", options.MODE)
}

View File

@@ -500,6 +500,8 @@ type HostIpmiAttributes struct {
IpmiPresent *bool `json:"ipmi_present"`
// lan channel
IpmiLanChannel *uint8 `json:"ipmi_lan_channel"`
// RMCP+ cipher suite for ipmitool -C
IpmiCipherSuite *int `json:"ipmi_cipher_suite"`
// verified
IpmiVerified *bool `json:"ipmi_verified"`
// Redfish API support

View File

@@ -205,7 +205,10 @@ func handleBaremetalValidateIPMI() appsrv.FilterHandler {
if redfishCli == nil {
resp.IsRedfishSupported = false
// use ipmitool to validate
tool := ipmitool.NewLanPlusIPMI(input.Ip, input.Username, input.Password)
tool, err := ipmitool.NewLanPlusIPMI(input.Ip, input.Username, input.Password)
if err != nil {
return nil, errors.Wrap(err, "NewLanPlusIPMI")
}
info, err := ipmitool.GetSysInfo(tool)
if err != nil {
return nil, errors.Wrap(err, "GetSysInfo by ipmitool")

View File

@@ -498,7 +498,10 @@ func (m *SBaremetalManager) checkSshInfo(input *BmRegisterInput) (*ssh.Client, e
}
func (m *SBaremetalManager) checkIpmiInfo(ctx context.Context, username, password, ipAddr string) (uint8, net.HardwareAddr, error) {
lanPlusTool := ipmitool.NewLanPlusIPMI(ipAddr, username, password)
lanPlusTool, err := ipmitool.NewLanPlusIPMI(ipAddr, username, password)
if err != nil {
return 0, nil, errors.Wrap(err, "NewLanPlusIPMI")
}
sysInfo, err := ipmitool.GetSysInfo(lanPlusTool)
if err != nil {
return 0, nil, errors.Wrap(err, "GetSysInfo")
@@ -1823,7 +1826,12 @@ func (b *SBaremetalInstance) GetIPMITool() *ipmitool.LanPlusIPMI {
log.Debugf("GetIPMIConfig is nil")
return nil
}
return ipmitool.NewLanPlusIPMI(conf.IpAddr, conf.Username, conf.Password)
tool, err := ipmitool.NewLanPlusIPMIWithCipher(conf.IpAddr, conf.Username, conf.Password, 623, conf.CipherSuite)
if err != nil {
log.Errorf("NewLanPlusIPMIWithCipher for %s: %v", conf.IpAddr, err)
return nil
}
return tool
}
func (b *SBaremetalInstance) isRedfishCapable() bool {

View File

@@ -537,7 +537,11 @@ func (task *sBaremetalPrepareTask) tryLocalIpmiAddr(ctx context.Context, sshIPMI
log.Errorf("Failed to get lan config after %d tries", tried)
return false
}
rmcpIPMI := ipmitool.NewLanPlusIPMI(tryAddr, ipmiUser, ipmiPasswd)
rmcpIPMI, err := ipmitool.NewLanPlusIPMI(tryAddr, ipmiUser, ipmiPasswd)
if err != nil {
log.Errorf("NewLanPlusIPMI for %s: %v", tryAddr, err)
return false
}
for tried = 0; tried < maxTries; tried += 1 {
conf2, err := ipmitool.GetLanConfig(rmcpIPMI, lanChannel)
if err != nil {

View File

@@ -85,7 +85,10 @@ func (self *SBaremetalIpmiProbeTask) DoIpmiProbe(ctx context.Context, args inter
// else, redfish call fails, try IPMI
} */
log.Warningf("BMC not redfish-compatible for IPMI: %s, use raw probe", ipmiInfo.IpAddr)
ipmiTool := ipmitool.NewLanPlusIPMI(ipmiInfo.IpAddr, ipmiInfo.Username, ipmiInfo.Password)
ipmiTool, err := ipmitool.NewLanPlusIPMI(ipmiInfo.IpAddr, ipmiInfo.Username, ipmiInfo.Password)
if err != nil {
return errors.Wrap(err, "NewLanPlusIPMI")
}
return self.doRawIpmiProbe(ctx, ipmiTool)
}
@@ -254,6 +257,9 @@ func (self *SBaremetalIpmiProbeTask) doRawIpmiProbe(ctx context.Context, cli ipm
ipmiInfo.CdromBoot = false
ipmiInfo.PxeBoot = o.Options.EnablePxeBoot
ipmiInfo.LanChannel = channel
if lanPlus, ok := cli.(*ipmitool.LanPlusIPMI); ok {
ipmiInfo.CipherSuite = lanPlus.GetCipherSuite()
}
updateData := jsonutils.Marshal(updateInfo)
updateData.(*jsonutils.JSONDict).Update(ipmiInfo.ToPrepareParams())
_, err = modules.Hosts.Update(self.Baremetal.GetClientSession(), self.Baremetal.GetId(), updateData)

View File

@@ -89,31 +89,60 @@ func (ipmi *SSHIPMI) ExecuteCommand(args ...string) ([]string, error) {
return ipmi.sshClient.Run(cmd.String())
}
// DefaultCipherSuites is the probe order for RMCP+ cipher suites.
// 0 means do not pass -C (ipmitool default); 3 and 17 are common BMC requirements.
var DefaultCipherSuites = []int{0, 3, 17}
type LanPlusIPMI struct {
IPMIParser
host string
user string
password string
port int
host string
user string
password string
port int
cipherSuite int // 0 = no -C; >0 = pass -C N
cipherResolved bool // true after Ensure/Detect or constructed with known suite > 0
}
func NewLanPlusIPMI(host, user, password string) *LanPlusIPMI {
func NewLanPlusIPMI(host, user, password string) (*LanPlusIPMI, error) {
return NewLanPlusIPMIWithPort(host, user, password, 623)
}
func NewLanPlusIPMIWithPort(host, user, password string, port int) *LanPlusIPMI {
return &LanPlusIPMI{
host: host,
user: user,
password: password,
port: port,
func NewLanPlusIPMIWithPort(host, user, password string, port int) (*LanPlusIPMI, error) {
return NewLanPlusIPMIWithCipher(host, user, password, port, 0)
}
func NewLanPlusIPMIWithCipher(host, user, password string, port, cipherSuite int) (*LanPlusIPMI, error) {
ipmi := &LanPlusIPMI{
host: host,
user: user,
password: password,
port: port,
cipherSuite: cipherSuite,
}
// Known non-default suite from persisted config: skip re-detect.
if cipherSuite > 0 {
ipmi.cipherResolved = true
return ipmi, nil
}
if err := ipmi.ensureCipherSuite(); err != nil {
return nil, err
}
return ipmi, nil
}
func (ipmi *LanPlusIPMI) GetMode() string {
return "rmcp"
}
func (ipmi *LanPlusIPMI) SetCipherSuite(suite int) {
ipmi.cipherSuite = suite
ipmi.cipherResolved = true
}
func (ipmi *LanPlusIPMI) GetCipherSuite() int {
return ipmi.cipherSuite
}
func (ipmi *LanPlusIPMI) GetCommand(args ...string) (*procutils.Command, context.CancelFunc) {
nArgs := []string{
"-I", "lanplus", "-H", ipmi.host,
@@ -121,6 +150,9 @@ func (ipmi *LanPlusIPMI) GetCommand(args ...string) (*procutils.Command, context
"-U", ipmi.user,
"-P", ipmi.password,
}
if ipmi.cipherSuite > 0 {
nArgs = append(nArgs, "-C", strconv.Itoa(ipmi.cipherSuite))
}
nArgs = append(nArgs, args...)
ctx, cancel := context.WithTimeout(context.Background(), ipmi.GetDefaultTimeout())
return procutils.NewCommandContext(ctx, "ipmitool", nArgs...), cancel
@@ -137,6 +169,34 @@ func (ipmi *LanPlusIPMI) ExecuteCommand(args ...string) ([]string, error) {
return ssh.ParseOutput(out), nil
}
// DetectCipherSuite tries DefaultCipherSuites with a single chassis power status each.
// On success it sets the working suite on the receiver and returns it.
func (ipmi *LanPlusIPMI) DetectCipherSuite() (int, error) {
var errs []error
for _, suite := range DefaultCipherSuites {
ipmi.cipherSuite = suite
ipmi.cipherResolved = false
_, err := ipmi.ExecuteCommand("chassis", "power", "status")
if err == nil {
ipmi.SetCipherSuite(suite)
log.Infof("[LanPlusIPMI] detected cipher suite %d for %s", suite, ipmi.host)
return suite, nil
}
errs = append(errs, errors.Wrapf(err, "cipher suite %d", suite))
log.Debugf("[LanPlusIPMI] cipher suite %d failed for %s: %v", suite, ipmi.host, err)
}
return 0, errors.Wrapf(errors.NewAggregate(errs), "detect cipher suite for %s", ipmi.host)
}
// ensureCipherSuite uses a known suite when already resolved; otherwise runs DetectCipherSuite.
func (ipmi *LanPlusIPMI) ensureCipherSuite() error {
if ipmi.cipherResolved {
return nil
}
_, err := ipmi.DetectCipherSuite()
return err
}
func GetSysGuid(exector IPMIExecutor) string {
args := []string{"mc", "guid"}
// args := []string{"raw", "0x06", "0x37"}

View File

@@ -24,15 +24,16 @@ const (
)
type SIPMIInfo struct {
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
IpAddr string `json:"ip_addr,omitempty"`
Present bool `json:"present,omitempty"`
LanChannel uint8 `json:"lan_channel,omitzero"`
Verified bool `json:"verified,omitfalse"`
RedfishApi bool `json:"redfish_api,omitfalse"`
CdromBoot bool `json:"cdrom_boot,omitfalse"`
PxeBoot bool `json:"pxe_boot,omitfalse"`
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
IpAddr string `json:"ip_addr,omitempty"`
Present bool `json:"present,omitempty"`
LanChannel uint8 `json:"lan_channel,omitzero"`
CipherSuite int `json:"cipher_suite,omitzero"`
Verified bool `json:"verified,omitfalse"`
RedfishApi bool `json:"redfish_api,omitfalse"`
CdromBoot bool `json:"cdrom_boot,omitfalse"`
PxeBoot bool `json:"pxe_boot,omitfalse"`
}
func (info SIPMIInfo) ToPrepareParams() jsonutils.JSONObject {
@@ -48,6 +49,9 @@ func (info SIPMIInfo) ToPrepareParams() jsonutils.JSONObject {
}
data.Add(jsonutils.NewBool(info.Present), "ipmi_present")
data.Add(jsonutils.NewInt(int64(info.LanChannel)), "ipmi_lan_channel")
if info.CipherSuite > 0 {
data.Add(jsonutils.NewInt(int64(info.CipherSuite)), "ipmi_cipher_suite")
}
if info.Verified {
data.Add(jsonutils.JSONTrue, "ipmi_verified")
}

View File

@@ -5077,6 +5077,9 @@ func fetchIpmiInfo(data api.HostIpmiAttributes, hostId string) (types.SIPMIInfo,
if data.IpmiLanChannel != nil {
info.LanChannel = *data.IpmiLanChannel
}
if data.IpmiCipherSuite != nil {
info.CipherSuite = *data.IpmiCipherSuite
}
if data.IpmiVerified != nil {
info.Verified = *data.IpmiVerified
}