From 0c0459650075a495807506a44bb534cfde2a347b Mon Sep 17 00:00:00 2001 From: Zexi Li Date: Tue, 28 Jul 2026 10:27:20 +0800 Subject: [PATCH] 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. --- build/docker/Dockerfile.baremetal-agent | 2 +- build/docker/Dockerfile.baremetal-base | 4 +- build/docker/Makefile | 6 +- cmd/ipmicli/main.go | 2 +- pkg/apis/compute/host.go | 2 + pkg/baremetal/handler/handlers.go | 5 +- pkg/baremetal/manager.go | 12 +++- pkg/baremetal/tasks/baseprepare.go | 6 +- pkg/baremetal/tasks/ipmiprobe.go | 8 ++- pkg/baremetal/utils/ipmitool/ipmitool.go | 82 ++++++++++++++++++++---- pkg/cloudcommon/types/ipmi.go | 22 ++++--- pkg/compute/models/hosts.go | 3 + 12 files changed, 123 insertions(+), 31 deletions(-) diff --git a/build/docker/Dockerfile.baremetal-agent b/build/docker/Dockerfile.baremetal-agent index efa3ef0fe1..b3f024d36e 100644 --- a/build/docker/Dockerfile.baremetal-agent +++ b/build/docker/Dockerfile.baremetal-agent @@ -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 " diff --git a/build/docker/Dockerfile.baremetal-base b/build/docker/Dockerfile.baremetal-base index 3331838984..60b8ad93e1 100644 --- a/build/docker/Dockerfile.baremetal-base +++ b/build/docker/Dockerfile.baremetal-base @@ -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-* diff --git a/build/docker/Makefile b/build/docker/Makefile index 39131c7140..96937deed3 100644 --- a/build/docker/Makefile +++ b/build/docker/Makefile @@ -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 diff --git a/cmd/ipmicli/main.go b/cmd/ipmicli/main.go index e2d94347e0..de0820c47e 100644 --- a/cmd/ipmicli/main.go +++ b/cmd/ipmicli/main.go @@ -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) } diff --git a/pkg/apis/compute/host.go b/pkg/apis/compute/host.go index 5651a42cdf..ec40ddbaf3 100644 --- a/pkg/apis/compute/host.go +++ b/pkg/apis/compute/host.go @@ -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 diff --git a/pkg/baremetal/handler/handlers.go b/pkg/baremetal/handler/handlers.go index 3d185d65aa..53bf47de3d 100644 --- a/pkg/baremetal/handler/handlers.go +++ b/pkg/baremetal/handler/handlers.go @@ -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") diff --git a/pkg/baremetal/manager.go b/pkg/baremetal/manager.go index 6706e120c8..015734d5c1 100644 --- a/pkg/baremetal/manager.go +++ b/pkg/baremetal/manager.go @@ -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 { diff --git a/pkg/baremetal/tasks/baseprepare.go b/pkg/baremetal/tasks/baseprepare.go index 8f3408eae9..f7b50c7ad5 100644 --- a/pkg/baremetal/tasks/baseprepare.go +++ b/pkg/baremetal/tasks/baseprepare.go @@ -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 { diff --git a/pkg/baremetal/tasks/ipmiprobe.go b/pkg/baremetal/tasks/ipmiprobe.go index c523798fbe..305b33255b 100644 --- a/pkg/baremetal/tasks/ipmiprobe.go +++ b/pkg/baremetal/tasks/ipmiprobe.go @@ -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) diff --git a/pkg/baremetal/utils/ipmitool/ipmitool.go b/pkg/baremetal/utils/ipmitool/ipmitool.go index f5184457aa..c6a334fad6 100644 --- a/pkg/baremetal/utils/ipmitool/ipmitool.go +++ b/pkg/baremetal/utils/ipmitool/ipmitool.go @@ -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"} diff --git a/pkg/cloudcommon/types/ipmi.go b/pkg/cloudcommon/types/ipmi.go index f9d0366d5b..c97a8162eb 100644 --- a/pkg/cloudcommon/types/ipmi.go +++ b/pkg/cloudcommon/types/ipmi.go @@ -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") } diff --git a/pkg/compute/models/hosts.go b/pkg/compute/models/hosts.go index c9abff8ae4..15ac18e70e 100644 --- a/pkg/compute/models/hosts.go +++ b/pkg/compute/models/hosts.go @@ -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 }