diff --git a/build/docker/Dockerfile.baremetal-agent b/build/docker/Dockerfile.baremetal-agent index 2385b4a78d..f076ce22d0 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.2 +FROM registry.cn-beijing.aliyuncs.com/yunionio/baremetal-base:v0.3 MAINTAINER "Zexi Li " diff --git a/build/docker/Dockerfile.baremetal-base b/build/docker/Dockerfile.baremetal-base index fb40e97c75..1fea0bef7b 100644 --- a/build/docker/Dockerfile.baremetal-base +++ b/build/docker/Dockerfile.baremetal-base @@ -1,7 +1,7 @@ -FROM registry.cn-beijing.aliyuncs.com/yunionio/centos-build:1.1-4 as build -RUN yum install -y https://iso.yunion.cn/3.6/rpms/packages/host/baremetal-pxerom-1.1.0-20072810.x86_64.rpm +FROM --platform=linux/amd64 registry.cn-beijing.aliyuncs.com/yunionio/centos-build:1.1-4 as build +RUN yum install -y https://iso.yunion.cn/vm-images/baremetal-pxerom-1.1.0-21060506.x86_64.rpm -FROM frolvlad/alpine-glibc:glibc-2.28 +FROM registry.cn-beijing.aliyuncs.com/yunionio/onecloud-base:v0.2 MAINTAINER "Yaoqi Wan " diff --git a/build/docker/Makefile b/build/docker/Makefile index cd49fec1f1..c9e77eab93 100644 --- a/build/docker/Makefile +++ b/build/docker/Makefile @@ -1,3 +1,8 @@ +REGISTRY ?= "registry.cn-beijing.aliyuncs.com/yunionio" + +DOCKER_BUILD = docker build -t $(REGISTRY) +DOCKER_BUILDX = docker buildx build --platform linux/arm64,linux/amd64 --push -t $(REGISTRY) + debian10-base: docker buildx build --platform linux/arm64,linux/amd64 --push \ -t registry.cn-beijing.aliyuncs.com/yunionio/debian10-base:1.0 -f ./Dockerfile.debian-base . @@ -5,3 +10,9 @@ debian10-base: climc-base: docker buildx build --platform linux/arm64,linux/amd64 --push \ -t registry.cn-beijing.aliyuncs.com/yunionio/climc-base:$(VERSION) -f ./Dockerfile.climc-base . + +BAREMETAL_BASE_VERSION = v0.3 + +baremetal-base: + $(DOCKER_BUILD)/baremetal-base:$(BAREMETAL_BASE_VERSION) -f ./Dockerfile.baremetal-base . + docker push $(REGISTRY)/baremetal-base:$(BAREMETAL_BASE_VERSION) diff --git a/pkg/baremetal/cronjobs.go b/pkg/baremetal/cronjobs.go index eab92d7675..8e5342febf 100644 --- a/pkg/baremetal/cronjobs.go +++ b/pkg/baremetal/cronjobs.go @@ -96,7 +96,7 @@ func (job *SStatusProbeJob) Do(ctx context.Context, now time.Time) error { if bStatus == api.BAREMETAL_READY || bStatus == api.BAREMETAL_RUNNING || bStatus == api.BAREMETAL_UNKNOWN { ps, err := job.baremetal.GetPowerStatus() if err != nil { - return errors.Wrap(err, "GetPowerStatus") + return errors.Wrap(err, "StatusProbeJob get power status") } job.lastTime = now pps := PowerStatusToBaremetalStatus(ps) diff --git a/pkg/baremetal/handler/handlers.go b/pkg/baremetal/handler/handlers.go index f4d1795796..42eed723ec 100644 --- a/pkg/baremetal/handler/handlers.go +++ b/pkg/baremetal/handler/handlers.go @@ -212,9 +212,13 @@ func handleServerStart(ctx *Context, bm *baremetal.SBaremetalInstance, _ baremet } func handleServerStop(ctx *Context, bm *baremetal.SBaremetalInstance, _ baremetaltypes.IBaremetalServer) { - if err := bm.StartServerStopTask(ctx.UserCred(), ctx.TaskId(), ctx.Data()); err != nil { - ctx.ResponseError(httperrors.NewGeneralError(err)) - return + if bm.HasBMC() { + if err := bm.StartServerStopTask(ctx.UserCred(), ctx.TaskId(), ctx.Data()); err != nil { + ctx.ResponseError(httperrors.NewGeneralError(err)) + return + } + } else { + bm.StartBaremetalMaintenanceTask(ctx.UserCred(), ctx.TaskId(), ctx.Data()) } ctx.ResponseOk() } diff --git a/pkg/baremetal/manager.go b/pkg/baremetal/manager.go index 4d9c24e024..9c5060112c 100644 --- a/pkg/baremetal/manager.go +++ b/pkg/baremetal/manager.go @@ -50,6 +50,7 @@ import ( "yunion.io/x/onecloud/pkg/baremetal/utils/disktool" "yunion.io/x/onecloud/pkg/baremetal/utils/ipmitool" raiddrivers "yunion.io/x/onecloud/pkg/baremetal/utils/raid/drivers" + "yunion.io/x/onecloud/pkg/baremetal/utils/uefi" "yunion.io/x/onecloud/pkg/cloudcommon/types" "yunion.io/x/onecloud/pkg/compute/baremetal" "yunion.io/x/onecloud/pkg/hostman/guestfs" @@ -797,7 +798,11 @@ func PowerStatusToServerStatus(bm *SBaremetalInstance, status string) string { if conf, _ := bm.GetSSHConfig(); conf == nil { return baremetalstatus.SERVER_RUNNING } else { - return baremetalstatus.SERVER_ADMIN + if !bm.HasBMC() && !bm.IsMaintenance() { + return baremetalstatus.SERVER_READY + } else { + return baremetalstatus.SERVER_ADMIN + } } case types.POWER_STATUS_OFF: return baremetalstatus.SERVER_READY @@ -927,13 +932,18 @@ func (b *SBaremetalInstance) NeedPXEBoot() bool { taskNeedPXEBoot = true } ret := false - if taskNeedPXEBoot || (task == nil && len(serverId) == 0 && b.GetHostType() == "baremetal") { + if taskNeedPXEBoot || (task == nil && len(serverId) == 0 && b.GetHostType() == "baremetal") || (task == nil && b.IsMaintenance()) { ret = true } log.Infof("Check task %s, server %s NeedPXEBoot: %v", taskName, serverId, ret) return ret } +func (b *SBaremetalInstance) IsMaintenance() bool { + isMt, _ := b.desc.Bool("is_maintenance") + return isMt +} + func (b *SBaremetalInstance) GetHostType() string { hostType, _ := b.desc.GetString("host_type") return hostType @@ -990,6 +1000,13 @@ func (b *SBaremetalInstance) getDHCPConfig( if err != nil { return nil, err } + if isPxe && IsUEFIPxeArch(arch) && !b.NeedPXEBoot() { + // TODO: use chainloader boot UEFI firmware, + // currently not response PXE request, + // and let BIOS detect bootable device + b.ClearSSHConfig() + return nil, errors.Errorf("Baremetal %s not need UEFI PXE boot", b.GetName()) + } return GetNicDHCPConfig(nic, serverIP.String(), hostName, isPxe, arch) } @@ -1290,6 +1307,19 @@ func (b *SBaremetalInstance) GetRawIPMIConfig() *types.SIPMIInfo { return &ipmiInfo } +func (b *SBaremetalInstance) GetUEFIInfo() (*types.EFIBootMgrInfo, error) { + key := "uefi_info" + if !b.desc.Contains(key) { + return nil, nil + } + info := new(types.EFIBootMgrInfo) + if err := b.desc.Unmarshal(info, key); err != nil { + + return nil, errors.Wrap(err, "Unmarshal json") + } + return info, nil +} + func (b *SBaremetalInstance) GetAccessIp() string { accessIp, _ := b.desc.GetString("access_ip") return accessIp @@ -1347,6 +1377,206 @@ func (b *SBaremetalInstance) SetExistingIPMIIPAddr(ipAddr string) { b.desc.Set("ipmi_info", info) } +func (b *SBaremetalInstance) HasBMC() bool { + conf := b.GetIPMIConfig() + if conf == nil { + return false + } + return true +} + +func (b *SBaremetalInstance) GetHostSSHClient() (*ssh.Client, error) { + conf, err := b.GetSSHConfig() + if err != nil { + return nil, errors.Wrap(err, "Get host ssh config") + } + if conf == nil { + return nil, errors.Errorf("Host ssh config is empty") + } + sshCli, err := ssh.NewClient(conf.RemoteIP, 22, "root", conf.Password, "") + if err != nil { + return nil, errors.Wrap(err, "New ssh client") + } + return sshCli, nil +} + +func (b *SBaremetalInstance) GetServerSSHClient() (*ssh.Client, error) { + s := b.GetServer() + if s == nil { + return nil, errors.Error("No server") + } + + privateKey, err := modules.Sshkeypairs.FetchPrivateKey(context.TODO(), auth.AdminCredential()) + if err != nil { + return nil, errors.Wrapf(err, "Get server %s login info", s.GetId()) + } + nics := s.GetNics() + var errs []error + for _, nic := range nics { + if nic.LinkUp && nic.Ip != "" { + for _, user := range []string{"cloudroot", "root"} { + sshCli, err := ssh.NewClient(nic.Ip, 22, user, "", privateKey) + if err != nil { + err = errors.Wrapf(err, "New server %s ssh client %s@%s", s.GetName(), user, nic.Ip) + errs = append(errs, err) + } else { + return sshCli, nil + } + } + } + } + + return nil, errors.NewAggregate(errs) +} + +func (b *SBaremetalInstance) SSHReachable() (bool, error) { + var errs []error + if cli, err := b.GetHostSSHClient(); err != nil { + errs = append(errs, err) + } else { + // host ssh reachable + cli.Close() + return true, nil + } + if cli, err := b.GetServerSSHClient(); err != nil { + errs = append(errs, err) + } else { + // server ssh reachable + cli.Close() + return true, nil + } + return false, errors.NewAggregate(errs) +} + +func (b *SBaremetalInstance) sshRunWrapper( + hostRun func(hostCli *ssh.Client) ([]string, error), + serverRun func(serverCli *ssh.Client) ([]string, error), +) ([]string, error) { + hostCli, err := b.GetHostSSHClient() + if err != nil { + log.Warningf("Get host ssh client error: %v", err) + } else { + if hostCli != nil { + return hostRun(hostCli) + } + } + + serverCli, err := b.GetServerSSHClient() + if err != nil { + return nil, errors.Wrapf(err, "Get baremetal %s server ssh client", b.GetName()) + } + return serverRun(serverCli) +} + +func (b *SBaremetalInstance) sshRun(hostCmd string, serverCmd string) ([]string, error) { + return b.sshRunWrapper( + func(hostCli *ssh.Client) ([]string, error) { + return hostCli.RawRun(hostCmd) + }, + func(serverCli *ssh.Client) ([]string, error) { + return serverCli.RunWithTTY(serverCmd) + }, + ) +} + +func (b *SBaremetalInstance) adjustUEFIWrapper(cli *ssh.Client, f func() error) error { + isUEFI, err := uefi.RemoteIsUEFIBoot(cli) + if err != nil { + return errors.Wrap(err, "Check is uefi boot") + } + if !isUEFI { + return nil + } + return f() +} + +func (b *SBaremetalInstance) AdjustUEFICurrentBootOrder(hostCli *ssh.Client) error { + return b.adjustUEFIWrapper(hostCli, func() error { + mgr, err := uefi.NewEFIBootMgrFromRemote(hostCli, false) + if err != nil { + return errors.Wrap(err, "NewEFIBootMgrFromRemote") + } + if err := uefi.RemoteSetCurrentBootAtFirst(hostCli, mgr); err != nil { + return errors.Wrap(err, "Set current boot order at first") + } + return b.SendUEFIInfo(mgr) + }) +} + +func (b *SBaremetalInstance) SendUEFIInfo(mgr *uefi.BootMgr) error { + info, err := mgr.ToEFIBootMgrInfo() + if err != nil { + return err + } + desc := b.desc + uefiData := jsonutils.Marshal(info) + desc.Add(uefiData, "uefi_info") + if err := b.SaveDesc(desc); err != nil { + return errors.Wrap(err, "Save uefi_info") + } + updateData := jsonutils.NewDict() + updateData.Add(uefiData, "uefi_info") + if _, err := modules.Hosts.Update(b.GetClientSession(), b.GetId(), updateData); err != nil { + return errors.Wrap(err, "Update cloud uefi info") + } + return nil +} + +func (b *SBaremetalInstance) adjustServerUEFIBootOrder(srvCli *ssh.Client) error { + return b.adjustUEFIWrapper(srvCli, func() error { + info, err := b.GetUEFIInfo() + if err != nil { + return errors.Wrap(err, "GetUEFIInfo from local desc") + } + if info == nil { + return uefi.RemoteTryToSetPXEBoot(srvCli) + } + if err := uefi.RemoteTryToSetPXEBoot(srvCli); err != nil { + log.Warningf("RemoteTryToSetPXEBoot error: %v", err) + } + _, err = uefi.RemoteSetBootOrderByInfo(srvCli, info.GetPXEEntry()) + return err + }) +} + +func (b *SBaremetalInstance) AdjustUEFIBootOrder() error { + _, err := b.sshRunWrapper( + func(hostCli *ssh.Client) ([]string, error) { + return nil, b.AdjustUEFICurrentBootOrder(hostCli) + }, + func(srvCli *ssh.Client) ([]string, error) { + return nil, b.adjustServerUEFIBootOrder(srvCli) + }, + ) + return err +} + +func (b *SBaremetalInstance) SSHReboot() error { + if !b.HasBMC() { + // try adjust uefi boot order before reboot + if err := b.AdjustUEFIBootOrder(); err != nil { + return errors.Wrap(err, "Adjust uefi boot order") + } + } + if _, err := b.sshRun("/sbin/reboot", "sudo shutdown -r now && exit"); err != nil { + if !ssh.IsExitMissingError(err) { + return errors.Wrap(err, "Try reboot") + } + } + b.ClearSSHConfig() + return nil +} + +func (b *SBaremetalInstance) SSHShutdown() error { + if _, err := b.sshRun("/sbin/poweroff", "sudo shutdown -h now && exit"); err != nil { + if ssh.IsExitMissingError(err) { + return nil + } + return errors.Wrap(err, "Try poweroff") + } + return nil +} + func (b *SBaremetalInstance) GetIPMITool() *ipmitool.LanPlusIPMI { conf := b.GetIPMIConfig() if conf == nil { @@ -1429,9 +1659,34 @@ func (b *SBaremetalInstance) DoDiskBoot() error { */ func (b *SBaremetalInstance) GetPowerStatus() (string, error) { + status, err := b.getPowerStatus() + if err != nil { + if errors.Cause(err) != types.ErrIPMIToolNull { + return "", errors.Wrap(err, "GetPowerStatus") + } else if b.HasBMC() { + return "", errors.Wrap(err, "GetPowerStatus from ipmi") + } + } + return status, nil +} + +func (b *SBaremetalInstance) getPowerStatus() (string, error) { ipmiCli := b.GetIPMITool() if ipmiCli == nil { - return "", fmt.Errorf("Baremetal %s ipmitool is nil", b.GetId()) + if cli, err := b.GetHostSSHClient(); err == nil { + cli.Close() + return types.POWER_STATUS_ON, nil + } else { + log.Warningf("Use host %s ssh client get powerstatus: %v", b.GetName(), err) + } + if cli, err := b.GetServerSSHClient(); err == nil { + cli.Close() + b.ClearSSHConfig() + return types.POWER_STATUS_ON, nil + } else { + log.Warningf("Use server %s ssh client get powerstatus: %v", b.GetServerName(), err) + } + return "", errors.Wrapf(types.ErrIPMIToolNull, "Baremetal %s", b.GetId()) } return ipmitool.GetChassisPowerStatus(ipmiCli) } @@ -2111,6 +2366,14 @@ func (s *SBaremetalServer) GetDiskConfig() ([]*api.BaremetalDiskConfig, error) { if err != nil { return nil, err } + + if len(layouts) != 0 { + firstDisk := layouts[0] + // convert to normal order if first disk is PCIE driver + if firstDisk.Conf.Driver == baremetal.DISK_DRIVER_PCIE { + return baremetal.GetLayoutDiskConfig(layouts), nil + } + } return baremetal.GetLayoutRaidConfig(layouts), nil } @@ -2131,7 +2394,9 @@ func (s *SBaremetalServer) DoDiskConfig(term *ssh.Client) error { if err != nil { return fmt.Errorf("CalculateLayout: %v", err) } + log.Errorf("===layouts: %s", jsonutils.Marshal(layouts).PrettyString()) diskConfs := baremetal.GroupLayoutResultsByDriverAdapter(layouts) + log.Errorf("===diskConfs: %s", jsonutils.Marshal(diskConfs).PrettyString()) for _, dConf := range diskConfs { driver := dConf.Driver raidDrv := raiddrivers.GetDriver(driver, term) diff --git a/pkg/baremetal/nic.go b/pkg/baremetal/nic.go index 6a23b216be..42d13fc79d 100644 --- a/pkg/baremetal/nic.go +++ b/pkg/baremetal/nic.go @@ -28,6 +28,30 @@ import ( "yunion.io/x/onecloud/pkg/util/dhcp" ) +const ( + // ref: https://datatracker.ietf.org/doc/html/rfc4578#section-2.1 + PXE_CLIENT_ARCH_INTEL_X86PC = iota + PXE_CLIENT_ARCH_NEC_PC98 + PXE_CLIENT_ARCH_EFI_ITANIUM + PXE_CLIENT_ARCH_DEC_ALPHA + PXE_CLIENT_ARCH_ARC_X86 + PXE_CLIENT_ARCH_INTEL_LEAN_CLIENT + PXE_CLIENT_ARCH_EFI_IA32 + PXE_CLIENT_ARCH_EFI_BC + PXE_CLIENT_ARCH_EFI_XSCALE + PXE_CLIENT_ARCH_EFI_X86_64 +) + +func IsUEFIPxeArch(arch uint16) bool { + switch arch { + case PXE_CLIENT_ARCH_EFI_IA32: + return true + case PXE_CLIENT_ARCH_EFI_BC, PXE_CLIENT_ARCH_EFI_XSCALE, PXE_CLIENT_ARCH_EFI_X86_64: + return true + } + return false +} + func GetNicDHCPConfig( n *types.SNic, serverIP string, @@ -71,9 +95,9 @@ func GetNicDHCPConfig( if isPxe { conf.BootServer = serverIP switch arch { - case 7, 9: + case PXE_CLIENT_ARCH_EFI_BC, PXE_CLIENT_ARCH_EFI_X86_64: conf.BootFile = "bootx64.efi" - case 6: + case PXE_CLIENT_ARCH_EFI_IA32: conf.BootFile = "bootia32.efi" default: //if o.Options.EnableTftpHttpDownload { diff --git a/pkg/baremetal/pxe/dhcp.go b/pkg/baremetal/pxe/dhcp.go index 1399e3bd8b..b6466d88b3 100644 --- a/pkg/baremetal/pxe/dhcp.go +++ b/pkg/baremetal/pxe/dhcp.go @@ -328,23 +328,23 @@ func (req *dhcpRequest) findBaremetalsOfAnyMac(session *mcclient.ClientSession, // createOrUpdateBaremetal create or update baremetal by client MAC func (req *dhcpRequest) createOrUpdateBaremetal(session *mcclient.ClientSession) (jsonutils.JSONObject, error) { - // first try UUID - ret, err := req.findBaremetalsByUuid(session) + // first try mac and is_baremetal=true + ret, err := req.findBaremetalsOfAnyMac(session, true) if err != nil { return nil, err } if len(ret.Data) == 0 { - // try mac and is_baremetal=true - ret, err = req.findBaremetalsOfAnyMac(session, true) + // try mac and host_type=baremetal + ret, err = req.findBaremetalsOfAnyMac(session, false) if err != nil { return nil, err } - if len(ret.Data) == 0 { - // try mac and host_type=baremetal - ret, err = req.findBaremetalsOfAnyMac(session, false) - if err != nil { - return nil, err - } + } + if len(ret.Data) == 0 { + // try UUID + ret, err = req.findBaremetalsByUuid(session) + if err != nil { + return nil, err } } switch len(ret.Data) { diff --git a/pkg/baremetal/tasks/base.go b/pkg/baremetal/tasks/base.go index b14e8a5256..81435429ca 100644 --- a/pkg/baremetal/tasks/base.go +++ b/pkg/baremetal/tasks/base.go @@ -342,6 +342,29 @@ func (self *SBaremetalTaskBase) EnsurePowerUp() error { return nil } +func (self *SBaremetalTaskBase) EnsureSSHReboot() error { + if err := self.Baremetal.SSHReboot(); err != nil { + return errors.Wrap(err, "Ensure ssh reboot") + } + + var ( + err error + canReach bool + ) + maxTries := 20 + startTime := time.Now() + for count := 0; count < maxTries; count++ { + times := (count % 10) + 1 + log.Infof("Try %s ssh connection after reboot %d times, %s passed", self.Baremetal.GetName(), times, time.Now().Sub(startTime)) + canReach, err = self.Baremetal.SSHReachable() + if canReach { + return nil + } + time.Sleep(10 * time.Second * time.Duration(times)) + } + return errors.Wrapf(err, "Test %s ssh connection after reboot", self.Baremetal.GetName()) +} + func (self *SBaremetalTaskBase) NeedPXEBoot() bool { return false } @@ -400,27 +423,35 @@ func (self *SBaremetalPXEBootTaskBase) InitPXEBootTask(ctx context.Context, args self.PxeBoot = false } - // Do soft reboot - if self.data != nil && jsonutils.QueryBoolean(self.data, "soft_boot", false) { - self.startTime = time.Now() - if err := self.Baremetal.DoPowerShutdown(true); err != nil { - // ignore error - log.Errorf("DoPowerShutdown error: %v", err) + if !self.Baremetal.HasBMC() { + // Try remote ssh reboot + if err := self.Baremetal.SSHReboot(); err != nil { + return errors.Wrap(err, "Try ssh reboot") } - //self.CallNextStage(self, self.WaitForShutdown, nil) - self.SetStage(self.WaitForShutdown) + } else { + // Do soft reboot + if self.data != nil && jsonutils.QueryBoolean(self.data, "soft_boot", false) { + self.startTime = time.Now() + if err := self.Baremetal.DoPowerShutdown(true); err != nil { + // ignore error + log.Errorf("DoPowerShutdown error: %v", err) + } + //self.CallNextStage(self, self.WaitForShutdown, nil) + self.SetStage(self.WaitForShutdown) - return nil + return nil + } + + // shutdown and power up to PXE mode + if err := self.EnsurePowerShutdown(false); err != nil { + return errors.Wrap(err, "EnsurePowerShutdown") + } + + if err := self.EnsurePowerUp(); err != nil { + return errors.Wrap(err, "EnsurePowerUp to pxe") + } } - // shutdown and power up to PXE mode - if err := self.EnsurePowerShutdown(false); err != nil { - return errors.Wrap(err, "EnsurePowerShutdown") - } - - if err := self.EnsurePowerUp(); err != nil { - return errors.Wrap(err, "EnsurePowerUp to pxe") - } // this stage will be called by baremetalInstance when pxe start notify self.SetSSHStage(self.IPXEBootTask().OnPXEBoot) return nil @@ -459,3 +490,7 @@ func (self *SBaremetalPXEBootTaskBase) OnStopComplete(ctx context.Context, args func (self *SBaremetalPXEBootTaskBase) GetName() string { return "BaremetalPXEBootTaskBase" } + +func AdjustUEFIBootOrder(term *ssh.Client, bm IBaremetal) error { + return bm.AdjustUEFICurrentBootOrder(term) +} diff --git a/pkg/baremetal/tasks/basedeploy.go b/pkg/baremetal/tasks/basedeploy.go index 0860de364c..fbf83cd324 100644 --- a/pkg/baremetal/tasks/basedeploy.go +++ b/pkg/baremetal/tasks/basedeploy.go @@ -22,6 +22,8 @@ import ( "yunion.io/x/pkg/errors" "yunion.io/x/pkg/utils" + "yunion.io/x/onecloud/pkg/baremetal/utils/uefi" + "yunion.io/x/onecloud/pkg/cloudcommon/types" "yunion.io/x/onecloud/pkg/mcclient" "yunion.io/x/onecloud/pkg/util/ssh" ) @@ -29,12 +31,15 @@ import ( type IServerBaseDeployTask interface { IPXEBootTask + RemoveEFIOSEntry() bool DoDeploys(term *ssh.Client) (jsonutils.JSONObject, error) PostDeploys(term *ssh.Client) error } type SBaremetalServerBaseDeployTask struct { SBaremetalPXEBootTaskBase + + needPXEBoot bool } func newBaremetalServerBaseDeployTask( @@ -45,6 +50,7 @@ func newBaremetalServerBaseDeployTask( ) SBaremetalServerBaseDeployTask { task := SBaremetalServerBaseDeployTask{ SBaremetalPXEBootTaskBase: newBaremetalPXEBootTaskBase(userCred, baremetal, taskId, data), + needPXEBoot: true, } // any inheritance must call: // task.SetStage(task.InitPXEBootTask) @@ -59,6 +65,10 @@ func (self *SBaremetalServerBaseDeployTask) GetName() string { return "BaremetalServerBaseDeployTask" } +func (self *SBaremetalServerBaseDeployTask) NeedPXEBoot() bool { + return self.needPXEBoot +} + func (self *SBaremetalServerBaseDeployTask) GetFinishAction() string { if self.data != nil { action, _ := self.data.GetString("on_finish") @@ -67,6 +77,10 @@ func (self *SBaremetalServerBaseDeployTask) GetFinishAction() string { return "" } +func (self *SBaremetalServerBaseDeployTask) RemoveEFIOSEntry() bool { + return false +} + func (self *SBaremetalServerBaseDeployTask) DoDeploys(_ *ssh.Client) (jsonutils.JSONObject, error) { return nil, nil } @@ -77,10 +91,22 @@ func (self *SBaremetalServerBaseDeployTask) PostDeploys(_ *ssh.Client) error { func (self *SBaremetalServerBaseDeployTask) OnPXEBoot(ctx context.Context, term *ssh.Client, args interface{}) error { log.Infof("%s called on stage pxeboot, args: %v", self.GetName(), args) + + if self.IServerBaseDeployTask().RemoveEFIOSEntry() { + if err := uefi.RemoteTryRemoveOSBootEntry(term); err != nil { + return errors.Wrap(err, "Remote uefi boot entry") + } + } + result, err := self.IServerBaseDeployTask().DoDeploys(term) if err != nil { return errors.Wrap(err, "Do deploy") } + + if err := AdjustUEFIBootOrder(term, self.Baremetal); err != nil { + return errors.Wrap(err, "Adjust UEFI boot order") + } + _, err = term.Run( "/bin/sync", "/sbin/sysctl -w vm.drop_caches=3", @@ -88,23 +114,42 @@ func (self *SBaremetalServerBaseDeployTask) OnPXEBoot(ctx context.Context, term if err != nil { return errors.Wrap(err, "Sync disk") } + if err := self.IServerBaseDeployTask().PostDeploys(term); err != nil { return errors.Wrap(err, "post deploy") } + onFinishAction := self.GetFinishAction() if utils.IsInStringArray(onFinishAction, []string{"restart", "shutdown"}) { - err = self.EnsurePowerShutdown(false) - if err != nil { - return errors.Wrap(err, "Ensure power off") - } - if onFinishAction == "restart" { - err = self.EnsurePowerUp() - if err != nil { - return errors.Wrap(err, "Ensure power up") + if self.Baremetal.HasBMC() { + if err := self.EnsurePowerShutdown(false); err != nil { + return errors.Wrap(err, "Ensure power off") + } + if onFinishAction == "restart" { + if err := self.EnsurePowerUp(); err != nil { + return errors.Wrap(err, "Ensure power up") + } + } + self.Baremetal.AutoSyncAllStatus() + } else { + if onFinishAction == "shutdown" { + log.Infof("None BMC baremetal can't shutdown when deploying") + /* + * if err := self.Baremetal.SSHShutdown(); err != nil { + * return errors.Wrap(err, "Try ssh shutdown") + * } + */ + } else { + // do restart + // hack: ssh reboot to disk + self.needPXEBoot = false + if err := self.EnsureSSHReboot(); err != nil { + return errors.Wrap(err, "Try ssh reboot") + } } } + self.Baremetal.SyncAllStatus(types.POWER_STATUS_ON) } - self.Baremetal.AutoSyncAllStatus() SetTaskComplete(self, result) return nil } diff --git a/pkg/baremetal/tasks/baseprepare.go b/pkg/baremetal/tasks/baseprepare.go index 7dcedd9f20..0507905c7c 100644 --- a/pkg/baremetal/tasks/baseprepare.go +++ b/pkg/baremetal/tasks/baseprepare.go @@ -137,22 +137,24 @@ func (task *sBaremetalPrepareTask) prepareBaremetalInfo(cli *ssh.Client) (*barem if ipmiInfo == nil { ipmiInfo = &types.SIPMIInfo{} } - if !ipmiInfo.Present && ipmiEnable { + if !ipmiInfo.Verified && !ipmiInfo.Present && ipmiEnable { ipmiInfo.Present = true ipmiInfo.Verified = false } - return &baremetalPrepareInfo{ - sysInfo, - cpuInfo, - dmiCPUInfo, - memInfo, - nicsInfo, - diskInfo, - storageDriver, - ipmiInfo, - isolatedDevicesInfo, - }, nil + prepareInfo := &baremetalPrepareInfo{ + sysInfo: sysInfo, + cpuInfo: cpuInfo, + dmiCpuInfo: dmiCPUInfo, + memInfo: memInfo, + nicsInfo: nicsInfo, + diskInfo: diskInfo, + storageDriver: storageDriver, + ipmiInfo: ipmiInfo, + isolatedDevicesInfo: isolatedDevicesInfo, + } + + return prepareInfo, nil } func (task *sBaremetalPrepareTask) configIPMISetting(cli *ssh.Client, i *baremetalPrepareInfo) error { @@ -313,7 +315,7 @@ func (task *sBaremetalPrepareTask) DoPrepare(cli *ssh.Client) error { // set ipmi nic address and user password if err = task.configIPMISetting(cli, infos); err != nil { logclient.AddActionLogWithStartable(task, task.baremetal, logclient.ACT_PREPARE, err, task.userCred, false) - return err + return errors.Wrap(err, "Config IPMI setting") } if err = task.updateBmInfo(cli, infos); err != nil { @@ -327,6 +329,11 @@ func (task *sBaremetalPrepareTask) DoPrepare(cli *ssh.Client) error { log.Errorf("SetNTP fail: %s", err) } + if err = AdjustUEFIBootOrder(cli, task.baremetal); err != nil { + logclient.AddActionLogWithStartable(task, task.baremetal, logclient.ACT_PREPARE, err, task.userCred, false) + return errors.Wrap(err, "Adjust UEFI boot order") + } + logclient.AddActionLogWithStartable(task, task.baremetal, logclient.ACT_PREPARE, infos.sysInfo, task.userCred, true) log.Infof("Prepare complete") @@ -358,7 +365,7 @@ func (task *sBaremetalPrepareTask) findAdminNic(cli *ssh.Client, nicsInfo []*typ func (task *sBaremetalPrepareTask) updateBmInfo(cli *ssh.Client, i *baremetalPrepareInfo) error { adminNic := task.baremetal.GetAdminNic() - if adminNic == nil { + if adminNic == nil || (adminNic != nil && !adminNic.LinkUp) { adminIdx, adminNicDev, err := task.findAdminNic(cli, i.nicsInfo) if err != nil { return errors.Wrap(err, "task.findAdminNic") @@ -368,7 +375,7 @@ func (task *sBaremetalPrepareTask) updateBmInfo(cli *ssh.Client, i *baremetalPre if err != nil { return errors.Wrap(err, "send Admin Nic Info") } - adminNic = task.baremetal.GetAdminNic() + adminNic = task.baremetal.GetNicByMac(adminNicDev.Mac) } // collect params updateInfo := make(map[string]interface{}) diff --git a/pkg/baremetal/tasks/create.go b/pkg/baremetal/tasks/create.go index e0c56186cf..250c44b2fa 100644 --- a/pkg/baremetal/tasks/create.go +++ b/pkg/baremetal/tasks/create.go @@ -46,6 +46,10 @@ func (self *SBaremetalServerCreateTask) GetName() string { return "BaremetalServerCreateTask" } +func (self *SBaremetalServerCreateTask) RemoveEFIOSEntry() bool { + return true +} + func (self *SBaremetalServerCreateTask) DoDeploys(term *ssh.Client) (jsonutils.JSONObject, error) { // Build raid err := self.Baremetal.GetServer().DoDiskConfig(term) @@ -90,7 +94,10 @@ func doPoweroff(term *ssh.Client) error { } func (self *SBaremetalServerCreateTask) PostDeploys(term *ssh.Client) error { - return doPoweroff(term) + if self.Baremetal.HasBMC() { + return doPoweroff(term) + } + return nil } func (self *SBaremetalServerCreateTask) onError(term *ssh.Client, err error) error { diff --git a/pkg/baremetal/tasks/deploy.go b/pkg/baremetal/tasks/deploy.go index c973369d4f..5fba72129d 100644 --- a/pkg/baremetal/tasks/deploy.go +++ b/pkg/baremetal/tasks/deploy.go @@ -43,6 +43,10 @@ func (self *SBaremetalServerDeployTask) GetName() string { return "BaremetalServerDeployTask" } +func (self *SBaremetalServerDeployTask) RemoveEFIOSEntry() bool { + return false +} + func (self *SBaremetalServerDeployTask) DoDeploys(term *ssh.Client) (jsonutils.JSONObject, error) { return self.Baremetal.GetServer().DoDeploy(term, self.data, false) } diff --git a/pkg/baremetal/tasks/destroy.go b/pkg/baremetal/tasks/destroy.go index 9469d9856e..172f7cf69d 100644 --- a/pkg/baremetal/tasks/destroy.go +++ b/pkg/baremetal/tasks/destroy.go @@ -44,6 +44,10 @@ func (self *SBaremetalServerDestroyTask) GetName() string { return "BaremetalServerDestroyTask" } +func (self *SBaremetalServerDestroyTask) RemoveEFIOSEntry() bool { + return true +} + func (self *SBaremetalServerDestroyTask) DoDeploys(term *ssh.Client) (jsonutils.JSONObject, error) { if err := self.Baremetal.GetServer().DoEraseDisk(term); err != nil { log.Errorf("Delete server do erase disk: %v", err) diff --git a/pkg/baremetal/tasks/interface.go b/pkg/baremetal/tasks/interface.go index 193ca6bb77..829d6a7486 100644 --- a/pkg/baremetal/tasks/interface.go +++ b/pkg/baremetal/tasks/interface.go @@ -23,6 +23,7 @@ import ( baremetaltypes "yunion.io/x/onecloud/pkg/baremetal/types" "yunion.io/x/onecloud/pkg/cloudcommon/types" "yunion.io/x/onecloud/pkg/mcclient" + "yunion.io/x/onecloud/pkg/util/ssh" ) type IBaremetal interface { @@ -68,6 +69,12 @@ type IBaremetal interface { SaveSSHConfig(remoteAddr string, key string) error ServerLoadDesc() error GetDHCPServerIP() (net.IP, error) + + HasBMC() bool + SSHReachable() (bool, error) + SSHReboot() error + SSHShutdown() error + AdjustUEFICurrentBootOrder(cli *ssh.Client) error } type IBmManager interface { diff --git a/pkg/baremetal/tasks/rebuild.go b/pkg/baremetal/tasks/rebuild.go index e8cb93f312..59d2eaafc1 100644 --- a/pkg/baremetal/tasks/rebuild.go +++ b/pkg/baremetal/tasks/rebuild.go @@ -45,6 +45,10 @@ func (self *SBaremetalServerRebuildTask) GetName() string { return "BaremetalServerRebuildTask" } +func (self *SBaremetalServerRebuildTask) RemoveEFIOSEntry() bool { + return true +} + func (self *SBaremetalServerRebuildTask) DoDeploys(term *ssh.Client) (jsonutils.JSONObject, error) { parts, err := self.Baremetal.GetServer().DoRebuildRootDisk(term) if err != nil { @@ -65,5 +69,8 @@ func (self *SBaremetalServerRebuildTask) DoDeploys(term *ssh.Client) (jsonutils. } func (self *SBaremetalServerRebuildTask) PostDeploys(term *ssh.Client) error { - return doPoweroff(term) + if self.Baremetal.HasBMC() { + return doPoweroff(term) + } + return nil } diff --git a/pkg/baremetal/tasks/reprepare.go b/pkg/baremetal/tasks/reprepare.go index b3d065afbf..16643ebddc 100644 --- a/pkg/baremetal/tasks/reprepare.go +++ b/pkg/baremetal/tasks/reprepare.go @@ -48,3 +48,8 @@ func (self *SBaremetalReprepareTask) DoDeploys(term *ssh.Client) (jsonutils.JSON err := task.DoPrepare(term) return nil, err } + +func (self *SBaremetalReprepareTask) PostDeploys(term *ssh.Client) error { + self.Baremetal.AutoSyncStatus() + return nil +} diff --git a/pkg/baremetal/tasks/start.go b/pkg/baremetal/tasks/start.go index e8014f5544..0452eea493 100644 --- a/pkg/baremetal/tasks/start.go +++ b/pkg/baremetal/tasks/start.go @@ -44,16 +44,22 @@ func NewBaremetalServerStartTask( } func (self *SBaremetalServerStartTask) DoBoot(ctx context.Context, args interface{}) error { - conf := self.Baremetal.GetRawIPMIConfig() - if !conf.CdromBoot { - err := self.Baremetal.DoPXEBoot() - if err != nil { - return errors.Wrap(err, "DoPXEBoot") + if self.Baremetal.HasBMC() { + conf := self.Baremetal.GetRawIPMIConfig() + if !conf.CdromBoot { + err := self.Baremetal.DoPXEBoot() + if err != nil { + return errors.Wrap(err, "DoPXEBoot") + } + } else { + err := self.Baremetal.DoRedfishPowerOn() + if err != nil { + return errors.Wrap(err, "DoRedfishPowerOn") + } } } else { - err := self.Baremetal.DoRedfishPowerOn() - if err != nil { - return errors.Wrap(err, "DoRedfishPowerOn") + if err := self.Baremetal.SSHReboot(); err != nil { + return errors.Wrap(err, "Try reboot") } } self.SetStage(self.WaitForStart) @@ -68,7 +74,7 @@ func (self *SBaremetalServerStartTask) GetName() string { func (self *SBaremetalServerStartTask) WaitForStart(ctx context.Context, args interface{}) error { status, err := self.Baremetal.GetPowerStatus() if err != nil { - return errors.Wrap(err, "GetPowerStatus") + return errors.Wrap(err, "Wait for start") } log.Infof("%s WaitForStart status=%s", self.GetName(), status) if status == types.POWER_STATUS_ON { diff --git a/pkg/baremetal/tasks/stop.go b/pkg/baremetal/tasks/stop.go index 5d1fd3bb97..d1a3298e44 100644 --- a/pkg/baremetal/tasks/stop.go +++ b/pkg/baremetal/tasks/stop.go @@ -46,9 +46,16 @@ func NewBaremetalServerStopTask( } func (task *SBaremetalServerStopTask) DoStop(ctx context.Context, args interface{}) error { - task.SetStage(task.WaitForStop) - if err := task.Baremetal.DoPowerShutdown(true); err != nil { - log.Errorf("Do power shutdown error: %s", err) + if task.Baremetal.HasBMC() { + task.SetStage(task.WaitForStop) + if err := task.Baremetal.DoPowerShutdown(true); err != nil { + log.Errorf("Do power shutdown error: %s", err) + } + } else { + if err := task.Baremetal.SSHShutdown(); err != nil { + return errors.Wrap(err, "Try ssh shutdown") + } + task.SetStage(task.OnStopComplete) } task.startTime = time.Now() ExecuteTask(task, nil) diff --git a/pkg/baremetal/tasks/unmaintenance.go b/pkg/baremetal/tasks/unmaintenance.go index ec51e2d848..cec48f9cbf 100644 --- a/pkg/baremetal/tasks/unmaintenance.go +++ b/pkg/baremetal/tasks/unmaintenance.go @@ -16,9 +16,9 @@ package tasks import ( "context" - "fmt" "yunion.io/x/jsonutils" + "yunion.io/x/pkg/errors" baremetalstatus "yunion.io/x/onecloud/pkg/baremetal/status" "yunion.io/x/onecloud/pkg/cloudcommon/types" @@ -45,14 +45,23 @@ func NewBaremetalUnmaintenanceTask( func (task *SBaremetalUnmaintenanceTask) DoUnmaintenance(ctx context.Context, args interface{}) error { var err error - if jsonutils.QueryBoolean(task.data, "guest_running", false) { - err = task.EnsurePowerShutdown(false) - if err != nil { - return fmt.Errorf("EnsurePowerShutdown hard: %v", err) - } - err = task.EnsurePowerUp() - if err != nil { - return fmt.Errorf("EnsurePowerUp disk: %v", err) + hasBMC := task.Baremetal.HasBMC() + if jsonutils.QueryBoolean(task.data, "guest_running", false) || !hasBMC { + if hasBMC { + err = task.EnsurePowerShutdown(false) + if err != nil { + return errors.Errorf("EnsurePowerShutdown hard: %v", err) + } + err = task.EnsurePowerUp() + if err != nil { + return errors.Errorf("EnsurePowerUp disk: %v", err) + } + } else { + if task.Baremetal.GetServer() != nil { + if err := task.EnsureSSHReboot(); err != nil { + return errors.Wrap(err, "Do unmaintenance for none BMC server") + } + } } task.Baremetal.SyncStatus(baremetalstatus.RUNNING, "") SetTaskComplete(task, nil) @@ -61,7 +70,7 @@ func (task *SBaremetalUnmaintenanceTask) DoUnmaintenance(ctx context.Context, ar task.SetStage(task.WaitForStop) err = task.EnsurePowerShutdown(true) if err != nil { - return fmt.Errorf("EnsurePowerShutdown soft: %v", err) + return errors.Errorf("EnsurePowerShutdown soft: %v", err) } ExecuteTask(task, nil) return nil diff --git a/pkg/baremetal/types/types.go b/pkg/baremetal/types/types.go index 6d3d23be1e..7e4cfad792 100644 --- a/pkg/baremetal/types/types.go +++ b/pkg/baremetal/types/types.go @@ -36,6 +36,7 @@ type IBaremetalServer interface { SyncPartitionSize(term *ssh.Client, parts []*disktool.Partition) ([]jsonutils.JSONObject, error) DoDeploy(term *ssh.Client, data jsonutils.JSONObject, isInit bool) (jsonutils.JSONObject, error) SaveDesc(desc jsonutils.JSONObject) error + GetNics() []types.SServerNic GetNicByMac(mac net.HardwareAddr) *types.SNic GetRootTemplateId() string diff --git a/pkg/baremetal/utils/disktool/disktool.go b/pkg/baremetal/utils/disktool/disktool.go index 5038c39508..9c3b7d24a5 100644 --- a/pkg/baremetal/utils/disktool/disktool.go +++ b/pkg/baremetal/utils/disktool/disktool.go @@ -616,7 +616,24 @@ func (tool *PartitionTool) parseLsDisk(lines []string, driver string) { } minCnt := int(math.Min(float64(len(disks)), float64(len(tool.diskTable[driver])))) for i := 0; i < minCnt; i++ { - tool.diskTable[driver][i].SetInfo(disks[i]) + driverDisk := tool.diskTable[driver][i] + if utils.IsInStringArray(driver, []string{NONRAID_DRIVER, PCIE_DRIVER}) { + // find size matched disk from disks + size := driverDisk.sizeMB + var remoteDisk *types.SDiskInfo + for ri := range disks { + if disks[ri].Size == size { + remoteDisk = disks[ri] + // remove matched disks + disks = append(disks[:ri], disks[ri+1:]...) + break + } + } + driverDisk.SetInfo(remoteDisk) + } else { + // raid disk set by order + driverDisk.SetInfo(disks[i]) + } } } diff --git a/pkg/baremetal/utils/uefi/doc.go b/pkg/baremetal/utils/uefi/doc.go new file mode 100644 index 0000000000..75fcbcaeb3 --- /dev/null +++ b/pkg/baremetal/utils/uefi/doc.go @@ -0,0 +1 @@ +package uefi // import "yunion.io/x/onecloud/pkg/baremetal/utils/uefi" diff --git a/pkg/baremetal/utils/uefi/uefi.go b/pkg/baremetal/utils/uefi/uefi.go new file mode 100644 index 0000000000..3f53faa145 --- /dev/null +++ b/pkg/baremetal/utils/uefi/uefi.go @@ -0,0 +1,478 @@ +// Copyright 2019 Yunion +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package uefi + +import ( + "fmt" + "strconv" + "strings" + + "yunion.io/x/log" + "yunion.io/x/pkg/errors" + + "yunion.io/x/onecloud/pkg/cloudcommon/types" + "yunion.io/x/onecloud/pkg/util/regutils2" + "yunion.io/x/onecloud/pkg/util/ssh" +) + +const ( + // efibootmgr useage: https://github.com/rhboot/efibootmgr + CMD_EFIBOOTMGR = "/usr/sbin/efibootmgr" + SUDO_EFIBOOTMGR = "sudo efibootmgr" + + MAC_KEYWORD = "MAC" +) + +type BootMgr struct { + // bootCurrent - the boot entry used to start the currently running system. + bootCurrent string + + // bootOrder - the boot order as would appear in the boot manager. + // The boot manager tries to boot the first active entry on this list. + // If unsuccessful, it tries the next entry, and so on. + bootOrder []string + + // bootNext - the boot entry which is scheduled to be run on next boot. + // This superceeds BootOrder for one boot only, and is deleted by the + // boot manager after first use. + // This allows you to change the next boot behavior without changing BootOrder. + bootNext string + + // timeout - the time in seconds between when the boot manager appears on the screen + // until when it automatically chooses the startup value from BootNext or BootOrder. + timeout int + + // entries - the boot entry parsed in map + entries map[string]*BootEntry +} + +type BootEntry struct { + BootNum string + Description string + IsActive bool +} + +func getEFIBootMgrCmd(sudo bool) string { + if sudo { + return SUDO_EFIBOOTMGR + } + return CMD_EFIBOOTMGR +} + +func ParseEFIBootMGR(input string) (*BootMgr, error) { + lines := strings.Split(input, "\n") + + mgr := &BootMgr{ + bootOrder: []string{}, + timeout: -1, + entries: make(map[string]*BootEntry), + } + + pf := func(ff func(string) bool) { + for _, l := range lines { + if ok := ff(l); ok { + break + } + } + } + + // parse BootCurrent + pf(func(l string) bool { + if current := parseEFIBootMGRBootCurrent(l); current != "" { + mgr.bootCurrent = current + return true + } + return false + }) + + // parse Timeout second + pf(func(l string) bool { + if timeout := parseEFIBootMGRTimeout(l); timeout != -1 { + mgr.timeout = timeout + return true + } + return false + }) + + // parse BootOrder + pf(func(l string) bool { + if order := parseEFIBootMGRBootOrder(l); len(order) != 0 { + mgr.bootOrder = order + return true + } + return false + }) + + // parse BootNext + pf(func(l string) bool { + if next := parseEFIBootMGRBootNext(l); next != "" { + mgr.bootNext = next + return true + } + return false + }) + + // parse entries + pf(func(l string) bool { + if entry := parseEFIBootMGREntry(l); entry != nil { + mgr.entries[entry.BootNum] = entry + } + return false + }) + + // finally check + if err := mgr.DataCheck(); err != nil { + return nil, errors.Wrap(err, "Invalid efibootmgr parse") + } + + return mgr, nil +} + +func (m *BootMgr) DataCheck() error { + if m.bootCurrent == "" { + return errors.Error("BootCurrent is empty") + } + + if len(m.bootOrder) == 0 { + return errors.Error("BootOrder length is 0") + } + + // check if BootOrder in entries + for _, orderNum := range m.bootOrder { + if _, ok := m.entries[orderNum]; !ok { + return errors.Errorf("Not found BootOrder %s entry", orderNum) + } + } + + return nil +} + +func parseEFIBootMGRBootCurrent(line string) string { + prefix := "BootCurrent: " + if strings.HasPrefix(line, prefix) { + return strings.Split(line, prefix)[1] + } + return "" +} + +func parseEFIBootMGRBootOrder(line string) []string { + prefix := "BootOrder: " + if !strings.HasPrefix(line, prefix) { + return nil + } + orderStr := strings.Split(line, prefix)[1] + return strings.Split(orderStr, ",") +} + +func parseEFIBootMGRBootNext(line string) string { + prefix := "BootNext: " + if !strings.HasPrefix(line, prefix) { + return "" + } + return strings.Split(line, prefix)[1] +} + +func parseEFIBootMGRTimeout(line string) int { + timeoutRegexp := `^Timeout: (?P[0-9]{1,}) seconds` + matches := regutils2.SubGroupMatch(timeoutRegexp, line) + if len(matches) == 0 { + return -1 + } + secondStr := matches["seconds"] + second, err := strconv.Atoi(secondStr) + if err != nil { + log.Errorf("parse %s seconds error: %v", secondStr, err) + return -1 + } + return second +} + +func parseEFIBootMGREntry(line string) *BootEntry { + entryRegexp := `^Boot(?P[0-9a-zA-Z]{4})[^:]+?\s+(?P.*)` + matches := regutils2.SubGroupMatch(entryRegexp, line) + if len(matches) == 0 { + return nil + } + num, ok := matches["num"] + if !ok { + return nil + } + desc, ok := matches["description"] + if !ok { + return nil + } + isActive := false + if strings.Contains(line, "* ") { + isActive = true + } + return &BootEntry{ + BootNum: num, + Description: desc, + IsActive: isActive, + } +} + +func NewEFIBootMgrFromRemote(cli *ssh.Client, sudo bool) (*BootMgr, error) { + return newEFIBootMgrFromRemote(cli, sudo, true) +} + +func newEFIBootMgrFromRemote(cli *ssh.Client, sudo bool, verbose bool) (*BootMgr, error) { + cmd := getEFIBootMgrCmd(sudo) + if verbose { + cmd = fmt.Sprintf("%s -v", cmd) + } + lines, err := cli.RawRun(cmd) + if err != nil { + return nil, errors.Wrapf(err, "Execute command: %s", cmd) + } + return ParseEFIBootMGR(strings.Join(lines, "\n")) +} + +func (m *BootMgr) GetCommand(sudo bool) string { + return getEFIBootMgrCmd(sudo) +} + +func (m *BootMgr) GetBootCurrent() string { + return m.bootCurrent +} + +func (m *BootMgr) GetBootOrder() []string { + return m.bootOrder +} + +func (m *BootMgr) GetBootNext() string { + return m.bootNext +} + +func (m *BootMgr) GetTimeout() int { + return m.timeout +} + +func (m *BootMgr) GetBootEntry(num string) *BootEntry { + return m.entries[num] +} + +func (m *BootMgr) GetBootEntryByDesc(desc string) *BootEntry { + for _, entry := range m.entries { + if strings.Contains(entry.Description, desc) { + return entry + } + } + return nil +} + +func (m *BootMgr) FindBootOrderPos(num string) int { + return stringArraryFindItemPos(m.bootOrder, num) +} + +func stringArraryFindItemPos(items []string, item string) int { + for idx, elem := range items { + if elem == item { + return idx + } + } + return -1 +} + +func stringArraryMove(items []string, item string, pos int) []string { + origPos := stringArraryFindItemPos(items, item) + if origPos == -1 { + items = append(items, item) + origPos = stringArraryFindItemPos(items, item) + } + + for i := origPos; i != pos; { + if i < pos { + // from left to right + tmp := items[i] + items[i] = items[i+1] + items[i+1] = tmp + i++ + } else if i > pos { + // from right to left + tmp := items[i] + items[i] = items[i-1] + items[i-1] = tmp + i-- + } + } + return items +} + +func (m *BootMgr) MoveBootOrder(num string, pos int) *BootMgr { + if entry := m.GetBootEntry(num); entry == nil { + log.Warningf("Not found boot entry by %q", num) + return m + } + m.bootOrder = stringArraryMove(m.bootOrder, num, pos) + return m +} + +func getSetBootOrderArgs(bootOrder []string) string { + return strings.Join(bootOrder, ",") +} + +func (m *BootMgr) GetSetBootOrderArgs() string { + return getSetBootOrderArgs(m.bootOrder) +} + +func RemoteIsUEFIBoot(cli *ssh.Client) (bool, error) { + checkCmd := "test -d /sys/firmware/efi && echo is || echo not" + lines, err := cli.Run(checkCmd) + if err != nil { + return false, err + } + for _, line := range lines { + if strings.Contains(line, "is") { + return true, nil + } + } + return false, nil +} + +func convertToEFIBootMgrInfo(info *BootMgr) (*types.EFIBootMgrInfo, error) { + data := &types.EFIBootMgrInfo{ + PxeBootNum: info.GetBootCurrent(), + BootOrder: make([]*types.EFIBootEntry, len(info.GetBootOrder())), + } + for idx, orderNum := range info.GetBootOrder() { + entry := info.GetBootEntry(orderNum) + if entry == nil { + return nil, errors.Errorf("Not found boot entry by %q", orderNum) + } + data.BootOrder[idx] = &types.EFIBootEntry{ + BootNum: entry.BootNum, + Description: entry.Description, + IsActive: entry.IsActive, + } + } + return data, nil +} + +func (mgr *BootMgr) ToEFIBootMgrInfo() (*types.EFIBootMgrInfo, error) { + return convertToEFIBootMgrInfo(mgr) +} + +func (mgr *BootMgr) sortEntryByKeyword(keyword string) *BootMgr { + newOrder := []string{} + oldOrder := []string{} + for _, num := range mgr.bootOrder { + entry := mgr.GetBootEntry(num) + if strings.Contains(entry.Description, keyword) { + newOrder = append(newOrder, num) + } else { + oldOrder = append(oldOrder, num) + } + } + newOrder = append(newOrder, oldOrder...) + mgr.bootOrder = newOrder + return mgr +} + +func RemoteSetCurrentBootAtFirst(cli *ssh.Client, mgr *BootMgr) error { + curPos := mgr.FindBootOrderPos(mgr.GetBootCurrent()) + if curPos == -1 { + return errors.Errorf("Not found BootCurrent position %q", mgr.GetBootCurrent()) + } + // move to first + mgr.MoveBootOrder(mgr.GetBootCurrent(), 0) + cmd := fmt.Sprintf("%s -o %s", mgr.GetCommand(false), mgr.GetSetBootOrderArgs()) + _, err := cli.Run(cmd) + return err +} + +func RemoteSetBootOrder(cli *ssh.Client, order []string) error { + cmd := fmt.Sprintf("%s -o %s", SUDO_EFIBOOTMGR, getSetBootOrderArgs(order)) + _, err := cli.RunWithTTY(cmd) + return err +} + +func RemoteSetBootOrderByInfo(cli *ssh.Client, entry *types.EFIBootEntry) (*BootMgr, error) { + mgr, err := newEFIBootMgrFromRemote(cli, true, true) + if err != nil { + return nil, err + } + + curEntry := mgr.GetBootEntryByDesc(entry.Description) + if curEntry == nil { + return nil, errors.Wrapf(err, "Not found remote boot entry by %q", entry.Description) + } + + mgr = mgr.sortEntryByKeyword(curEntry.Description) + return mgr, RemoteSetBootOrder(cli, mgr.GetBootOrder()) +} + +func RemoteTryToSetPXEBoot(cli *ssh.Client) error { + mgr, err := newEFIBootMgrFromRemote(cli, true, true) + if err != nil { + return err + } + + mgr = mgr.sortEntryByKeyword(MAC_KEYWORD) + return RemoteSetBootOrder(cli, mgr.GetBootOrder()) +} + +func remoteISUEFIBootWrap(cli *ssh.Client, f func(*ssh.Client) error) error { + isUEFI, err := RemoteIsUEFIBoot(cli) + if err != nil { + return errors.Wrap(err, "Check is UEFI boot") + } + if !isUEFI { + return nil + } + return f(cli) +} + +func RemoteTryRemoveOSBootEntry(hostCli *ssh.Client) error { + return remoteISUEFIBootWrap(hostCli, remoteTryRemoveOSBootEntry) +} + +func remoteTryRemoveOSBootEntry(hostCli *ssh.Client) error { + mgr, err := newEFIBootMgrFromRemote(hostCli, false, true) + if err != nil { + return err + } + + // TODO: find other ways to decide whether entry is OS boot + osKeywords := []string{ + "linux", + "centos", + "ubuntu", + "windows", + "grub", + } + + isOsEntry := func(desc string) bool { + for _, key := range osKeywords { + desc := strings.ToLower(desc) + if strings.Contains(desc, key) { + return true + } + } + return false + } + + for _, entry := range mgr.entries { + if !isOsEntry(entry.Description) { + continue + } + // delete entry and remove it from BootOrder + cmd := fmt.Sprintf("%s -b %s -B", mgr.GetCommand(false), entry.BootNum) + if _, err := hostCli.Run(cmd); err != nil { + return errors.Wrapf(err, "remove boot entry: %s", entry.Description) + } + } + return nil +} diff --git a/pkg/baremetal/utils/uefi/uefi_test.go b/pkg/baremetal/utils/uefi/uefi_test.go new file mode 100644 index 0000000000..25016d457d --- /dev/null +++ b/pkg/baremetal/utils/uefi/uefi_test.go @@ -0,0 +1,316 @@ +// Copyright 2019 Yunion +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package uefi + +import ( + "reflect" + "testing" + + "github.com/stretchr/testify/assert" +) + +const ( + TestPCOutput = `BootCurrent: 0007 +Timeout: 2 seconds +BootOrder: 0003,0002,0001,0000,0004,0005,0007,0008,0009,000A +Boot0000 Windows Boot Manager +Boot0001 ubuntu +Boot0002 ubuntu +Boot0003 CentOS Linux +Boot0004* Onboard NIC(IPV4) +Boot0005 Onboard NIC(IPV6) +Boot0007* PXE IPv4 Intel(R) Ethernet Server Adapter X520-2 +Boot0008 PXE IPv6 Intel(R) Ethernet Server Adapter X520-2 +Boot0009* PXE IPv4 Intel(R) Ethernet Server Adapter X520-2 +Boot000A PXE IPv6 Intel(R) Ethernet Server Adapter X520-2` + + TestQemuOutput = `BootCurrent: 0005 +Timeout: 0 seconds +BootOrder: 0005,0009,0008,0007,0002,0003,0004,0006,0001,0000 +Boot0000* UiApp +Boot0001* UEFI QEMU DVD-ROM QM00003 +Boot0002* UEFI Floppy +Boot0003* UEFI Floppy 2 +Boot0004* UEFI QEMU HARDDISK QM00001 +Boot0005* UEFI PXEv4 (MAC:525400123456) +Boot0006* EFI Internal Shell +Boot0007* CentOS +Boot0008* CentOS Linux +Boot0009* ubuntu` +) + +func Test_parseTimeout(t *testing.T) { + tests := []struct { + input string + want int + }{ + { + input: "Timeout: 0 seconds", + want: 0, + }, + { + input: "Timeout: 30 seconds", + want: 30, + }, + { + input: "Timeout: seconds", + want: -1, + }, + } + + for _, tc := range tests { + if got := parseEFIBootMGRTimeout(tc.input); !reflect.DeepEqual(got, tc.want) { + t.Errorf("parseEFIBootMGRTimeout() = %v, want %v", got, tc.want) + } + } +} + +func Test_parseEFIBootMGRBootOrder(t *testing.T) { + tests := []struct { + input string + want []string + }{ + { + input: "BootOrder: 0005,0009,0008,0007,0002,0003,0004,0006,0001,0000", + want: []string{"0005", "0009", "0008", "0007", "0002", "0003", "0004", "0006", "0001", "0000"}, + }, + } + + for _, tc := range tests { + if got := parseEFIBootMGRBootOrder(tc.input); !reflect.DeepEqual(got, tc.want) { + t.Errorf("parseEFIBootMGRBootOrder() = %v, want %v", got, tc.want) + } + } +} + +func Test_parseEFIBootMGRBootCurrent(t *testing.T) { + tests := []struct { + name string + line string + want string + }{ + { + name: "BootCurrent: 0005", + line: "BootCurrent: 0005", + want: "0005", + }, + { + name: "BootCurrent: ", + line: "BootCurrent: ", + want: "", + }, + { + name: "BootCurrent:", + line: "BootCurrent:", + want: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := parseEFIBootMGRBootCurrent(tt.line); got != tt.want { + t.Errorf("parseEFIBootMGRBootCurrent() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_parseEFIBootMGREntry(t *testing.T) { + tests := []struct { + name string + line string + want *BootEntry + }{ + { + name: "Boot0009* ubuntu", + line: "Boot0009* ubuntu", + want: &BootEntry{ + BootNum: "0009", + Description: "ubuntu", + IsActive: true, + }, + }, + { + name: "Boot0005* UEFI PXEv4 (MAC:525400123456)", + line: "Boot0005* UEFI PXEv4 (MAC:525400123456)", + want: &BootEntry{ + BootNum: "0005", + Description: "UEFI PXEv4 (MAC:525400123456)", + IsActive: true, + }, + }, + { + name: "Boot0003 CentOS Linux", + line: "Boot0003 CentOS Linux", + want: &BootEntry{ + BootNum: "0003", + Description: "CentOS Linux", + IsActive: false, + }, + }, + { + name: "Timeout: 2 seconds", + line: "Timeout: 2 seconds", + want: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := parseEFIBootMGREntry(tt.line); !reflect.DeepEqual(got, tt.want) { + t.Errorf("parseEFIBootMGREntry() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestParseEFIBootMGR(t *testing.T) { + tests := []struct { + name string + input string + want *BootMgr + wantErr bool + }{ + { + name: "Parse PC output", + input: TestPCOutput, + want: &BootMgr{ + bootCurrent: "0007", + timeout: 2, + bootOrder: []string{"0003", "0002", "0001", "0000", "0004", "0005", "0007", "0008", "0009", "000A"}, + entries: map[string]*BootEntry{ + "0000": { + BootNum: "0000", + IsActive: false, + Description: "Windows Boot Manager", + }, + "0001": { + BootNum: "0001", + IsActive: false, + Description: "ubuntu", + }, + "0002": { + BootNum: "0002", + IsActive: false, + Description: "ubuntu", + }, + "0003": { + BootNum: "0003", + IsActive: false, + Description: "CentOS Linux", + }, + "0004": { + BootNum: "0004", + IsActive: true, + Description: "Onboard NIC(IPV4)", + }, + "0005": { + BootNum: "0005", + IsActive: false, + Description: "Onboard NIC(IPV6)", + }, + "0007": { + BootNum: "0007", + IsActive: true, + Description: "PXE IPv4 Intel(R) Ethernet Server Adapter X520-2", + }, + "0008": { + BootNum: "0008", + IsActive: false, + Description: "PXE IPv6 Intel(R) Ethernet Server Adapter X520-2", + }, + "0009": { + BootNum: "0009", + IsActive: true, + Description: "PXE IPv4 Intel(R) Ethernet Server Adapter X520-2", + }, + "000A": { + BootNum: "000A", + IsActive: false, + Description: "PXE IPv6 Intel(R) Ethernet Server Adapter X520-2", + }, + }}, + }, + } + + assert := assert.New(t) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ParseEFIBootMGR(tt.input) + if (err != nil) != tt.wantErr { + t.Errorf("ParseEFIBootMGR() error = %v, wantErr %v", err, tt.wantErr) + return + } + if equal := assert.Equal(tt.want, got); !equal { + t.Errorf("ParseEFIBootMGR() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_stringArraryMove(t *testing.T) { + type args struct { + items []string + item string + pos int + } + tests := []struct { + name string + args args + want []string + }{ + { + name: "move left to right", + args: args{ + items: []string{"1", "2", "3"}, + item: "3", + pos: 0, + }, + want: []string{"3", "1", "2"}, + }, + { + name: "move right to left", + args: args{ + items: []string{"1", "2", "3"}, + item: "1", + pos: 2, + }, + want: []string{"2", "3", "1"}, + }, + { + name: "no move", + args: args{ + items: []string{"1", "2", "3"}, + item: "2", + pos: 1, + }, + want: []string{"1", "2", "3"}, + }, + { + name: "add item move", + args: args{ + items: []string{"1", "2", "3"}, + item: "4", + pos: 0, + }, + want: []string{"4", "1", "2", "3"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := stringArraryMove(tt.args.items, tt.args.item, tt.args.pos); !reflect.DeepEqual(got, tt.want) { + t.Errorf("stringArraryMove() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/cloudcommon/types/types.go b/pkg/cloudcommon/types/types.go index 65d1366f3d..e71ccf70de 100644 --- a/pkg/cloudcommon/types/types.go +++ b/pkg/cloudcommon/types/types.go @@ -17,6 +17,12 @@ package types import ( "net" "strings" + + "yunion.io/x/pkg/errors" +) + +const ( + ErrIPMIToolNull = errors.Error("IPMI tool is null") ) type SSHConfig struct { diff --git a/pkg/cloudcommon/types/uefi.go b/pkg/cloudcommon/types/uefi.go new file mode 100644 index 0000000000..fa88a40cb7 --- /dev/null +++ b/pkg/cloudcommon/types/uefi.go @@ -0,0 +1,35 @@ +// Copyright 2019 Yunion +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +type EFIBootMgrInfo struct { + PxeBootNum string `json:"pxe_boot_num"` + BootOrder []*EFIBootEntry `json:"boot_order"` +} + +type EFIBootEntry struct { + BootNum string `json:"boot_num"` + Description string `json:"description"` + IsActive bool `json:"is_active"` +} + +func (info *EFIBootMgrInfo) GetPXEEntry() *EFIBootEntry { + for _, e := range info.BootOrder { + if e.BootNum == info.PxeBootNum { + return e + } + } + return nil +} diff --git a/pkg/compute/baremetal/diskconfig.go b/pkg/compute/baremetal/diskconfig.go index 9cd552c139..8623bdd198 100644 --- a/pkg/compute/baremetal/diskconfig.go +++ b/pkg/compute/baremetal/diskconfig.go @@ -58,7 +58,7 @@ func isDiskConfigStorageMatch( adapterIsEqual := (confAdapter == nil || *confAdapter == adapter) && (confDriver == nil || *confDriver == driver) - log.V(10).Debugf("typeIsHybrid: %v, typeIsRotate: %v, typeIsSSD: %v, rangeIsNoneAndCountZero: %v, rangeIsNotNoneAndIndexInRange: %v, rangeIsNoneAndSmallThanCount: %v, adapterIsEqual: %v", typeIsHybrid, typeIsRotate, typeIsSSD, rangeIsNoneAndCountZero, rangeIsNotNoneAndIndexInRange, rangeIsNoneAndSmallThanCount, adapterIsEqual) + log.V(10).Debugf("Try storage: %#v, typeIsHybrid: %v, typeIsRotate: %v, typeIsSSD: %v, rangeIsNoneAndCountZero: %v, rangeIsNotNoneAndIndexInRange: %v, rangeIsNoneAndSmallThanCount: %v, adapterIsEqual: %v", *storage, typeIsHybrid, typeIsRotate, typeIsSSD, rangeIsNoneAndCountZero, rangeIsNotNoneAndIndexInRange, rangeIsNoneAndSmallThanCount, adapterIsEqual) if (typeIsHybrid || typeIsRotate || typeIsSSD) && (rangeIsNoneAndCountZero || rangeIsNotNoneAndIndexInRange || rangeIsNoneAndSmallThanCount) && @@ -109,7 +109,11 @@ func RetrieveStorages(diskConfig *api.BaremetalDiskConfig, storages []*Baremetal } else { rest = append(rest, storage) } - idx++ + if confDriver == nil { + idx++ + } else if *confDriver == storage.Driver { + idx++ + } } return } @@ -298,10 +302,18 @@ func ExpandNoneConf(layouts []Layout) (ret []Layout) { } func GetLayoutRaidConfig(layouts []Layout) []*api.BaremetalDiskConfig { + return getLayoutConfig(layouts, true) +} + +func GetLayoutDiskConfig(layouts []Layout) []*api.BaremetalDiskConfig { + return getLayoutConfig(layouts, false) +} + +func getLayoutConfig(layouts []Layout, onlyRaidDisk bool) []*api.BaremetalDiskConfig { var disk []*BaremetalStorage ret := make([]*api.BaremetalDiskConfig, 0) for _, layout := range layouts { - if layout.Conf.Conf == DISK_CONF_NONE && + if onlyRaidDisk && layout.Conf.Conf == DISK_CONF_NONE && sets.NewString(DISK_DRIVER_LINUX, DISK_DRIVER_PCIE).Has(layout.Disks[0].Driver) { continue } @@ -335,8 +347,8 @@ func CalculateLayout(confs []*api.BaremetalDiskConfig, storages []*BaremetalStor noneConf, _ := ParseDiskConfig(DISK_CONF_NONE) conf = &noneConf } - selected, storage1 := RetrieveStorages(conf, storages) - storages = storage1 + selected, restStorges := RetrieveStorages(conf, storages) + storages = restStorges if len(selected) == 0 { err = fmt.Errorf("Not found matched storages by config: %#v", conf) return diff --git a/pkg/compute/baremetal/diskconfig_test.go b/pkg/compute/baremetal/diskconfig_test.go index f1ddb562e3..5b796f17a3 100644 --- a/pkg/compute/baremetal/diskconfig_test.go +++ b/pkg/compute/baremetal/diskconfig_test.go @@ -19,6 +19,11 @@ import ( "reflect" "testing" + "github.com/stretchr/testify/assert" + + "yunion.io/x/jsonutils" + "yunion.io/x/log" + api "yunion.io/x/onecloud/pkg/apis/compute" ) @@ -464,7 +469,9 @@ func TestCalculateLayout(t *testing.T) { if err != nil { t.Fatalf("Unmarshal expectedLayoutJson err: %v", err) } - if !reflect.DeepEqual(layout, expectedLayout) { + + assert := assert.New(t) + if !assert.Equal(expectedLayout, layout) { t.Errorf("CalculateLayout() = %v, want %v", layout, expectedLayout) } } @@ -475,7 +482,7 @@ func TestCheckDisksAllocable(t *testing.T) { "6:raid5:adapter2", "6:raid5:adapter2", ) - bitmainConfs, err := NewBaremetalDiskConfigs("raid10:(60g,)") + bitmainConfs, err := NewBaremetalDiskConfigs("MarvelRaid:raid10:(60g,)") if err != nil { t.Fatalf("NewDiskConfigs err: %v", err) } @@ -1255,3 +1262,133 @@ func TestGetSplitSizes(t *testing.T) { }) } } + +var ( + pcieStorages = []*BaremetalStorage{ + { + Driver: DISK_DRIVER_LINUX, + Rotate: true, + Size: 51200, + Adapter: 0, + Index: 0, + }, + { + Driver: DISK_DRIVER_PCIE, + Rotate: false, + Size: 61440, + Adapter: 0, + Index: 0, + }, + } + + pcie2Storages = []*BaremetalStorage{ + { + Driver: DISK_DRIVER_LINUX, + Dev: "sda", + Rotate: true, + Size: 3815447, + Adapter: 0, + Index: 0, + }, + { + Driver: DISK_DRIVER_PCIE, + Dev: "nvme0n1", + Rotate: false, + Size: 1953514, + Adapter: 0, + Index: 0, + }, + { + Driver: DISK_DRIVER_PCIE, + Dev: "nvme1n1", + Rotate: false, + Size: 122104, + Adapter: 0, + Index: 0, + }, + } +) + +func TestPCIEStoragesAllocable(t *testing.T) { + adapter0 := 0 + confs := []*api.BaremetalDiskConfig{ + { + Adapter: &adapter0, + Conf: DISK_CONF_NONE, + Count: 1, + Driver: DISK_DRIVER_PCIE, + Range: []int64{0}, + Type: DISK_TYPE_SSD, + }, + { + Adapter: &adapter0, + Conf: DISK_CONF_NONE, + Count: 1, + Driver: DISK_DRIVER_LINUX, + Range: []int64{0}, + Type: DISK_TYPE_ROTATE, + }, + } + + layouts, err := CalculateLayout(confs, pcieStorages) + if err != nil { + t.Errorf("CalculateLayout error: %v", err) + return + } + // log.Errorf("layouts: %s", jsonutils.Marshal(layouts)) + disks := []*api.DiskConfig{ + { + Backend: api.STORAGE_LOCAL, + Driver: "scsi", + SizeMb: 30720, + }, + { + Backend: api.STORAGE_LOCAL, + Driver: "scsi", + Fs: "ext4", + Mountpoint: "/opt", + SizeMb: -1, + }, + { + Backend: api.STORAGE_LOCAL, + Driver: "scsi", + Fs: "ext4", + Mountpoint: "/data", + SizeMb: -1, + }, + } + if ok := IsDisksAllocable(layouts, disks); !ok { + t.Errorf("Disk not allocable") + } +} + +func Test2PCIEStoragesAllocable(t *testing.T) { + adapter0 := 0 + confs := []*api.BaremetalDiskConfig{ + { + Adapter: &adapter0, + Conf: DISK_CONF_NONE, + Count: 0, + Driver: DISK_DRIVER_PCIE, + Range: []int64{1}, + Type: DISK_TYPE_SSD, + }, + } + + layouts, err := CalculateLayout(confs, pcie2Storages) + if err != nil { + t.Errorf("CalculateLayout error: %v", err) + return + } + log.Errorf("layouts: %s", jsonutils.Marshal(layouts)) + disks := []*api.DiskConfig{ + { + Backend: api.STORAGE_LOCAL, + Driver: "scsi", + SizeMb: -1, + }, + } + if ok := IsDisksAllocable(layouts, disks); !ok { + t.Errorf("Disk not allocable") + } +} diff --git a/pkg/compute/guestdrivers/baremetals.go b/pkg/compute/guestdrivers/baremetals.go index be4f75ec24..9acb197b39 100644 --- a/pkg/compute/guestdrivers/baremetals.go +++ b/pkg/compute/guestdrivers/baremetals.go @@ -345,7 +345,7 @@ func (self *SBaremetalGuestDriver) RequestStopGuestForDelete(ctx context.Context return guest.StartGuestStopTask(ctx, task.GetUserCred(), true, false, task.GetTaskId()) } if host != nil && !host.GetEnabled() && !purge { - return fmt.Errorf("fail to contact baremetal") + return errors.Errorf("fail to contact baremetal") } task.ScheduleRun(nil) return nil diff --git a/pkg/compute/models/hosts.go b/pkg/compute/models/hosts.go index b80f7f1a42..295da2b643 100644 --- a/pkg/compute/models/hosts.go +++ b/pkg/compute/models/hosts.go @@ -188,6 +188,9 @@ type SHost struct { // IPv4地址,作为么有云vpc访问外网时的网关 OvnMappedIpAddr string `width:"16" charset:"ascii" nullable:"true" list:"user"` + + // UEFI详情 + UefiInfo jsonutils.JSONObject `nullable:"true" get:"domain" update:"domain" create:"domain_optional"` } func (manager *SHostManager) GetContextManagers() [][]db.IModelManager { @@ -3534,6 +3537,10 @@ func (self *SHost) PostUpdate(ctx context.Context, userCred mcclient.TokenCreden log.Errorf("baremetal host %s update related server %s spec error: %v", self.GetName(), guest.GetName(), err) } } + + if err := self.startSyncConfig(ctx, userCred, "", true); err != nil { + log.Errorf("start sync host %q config after updated", self.GetName()) + } } func (self *SHost) UpdateDnsRecords(isAdd bool) { @@ -3939,6 +3946,25 @@ func (self *SHost) AllowPerformPrepare(ctx context.Context, return db.IsAdminAllowPerform(userCred, self, "prepare") } +func (self *SHost) HasBMC() bool { + ipmiInfo, _ := self.GetIpmiInfo() + if ipmiInfo.Username != "" && ipmiInfo.Password != "" { + return true + } + return false +} + +func (self *SHost) IsUEFIBoot() bool { + info, _ := self.GetUEFIInfo() + if info == nil { + return false + } + if len(info.PxeBootNum) == 0 { + return false + } + return true +} + func (self *SHost) isRedfishCapable() bool { ipmiInfo, _ := self.GetIpmiInfo() if ipmiInfo.Verified && ipmiInfo.RedfishApi { @@ -5623,6 +5649,17 @@ func (host *SHost) GetIpmiInfo() (types.SIPMIInfo, error) { return info, nil } +func (host *SHost) GetUEFIInfo() (*types.EFIBootMgrInfo, error) { + if host.UefiInfo == nil { + return nil, nil + } + info := new(types.EFIBootMgrInfo) + if err := host.UefiInfo.Unmarshal(info); err != nil { + return nil, errors.Wrap(err, "host.UefiInfo.Unmarshal") + } + return info, nil +} + func (self *SHost) AllowGetDetailsJnlp(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject) bool { return db.IsAdminAllowGetSpec(userCred, self, "jnlp") } @@ -5724,7 +5761,13 @@ func (self *SHost) PerformSyncConfig(ctx context.Context, userCred mcclient.Toke } func (self *SHost) StartSyncConfig(ctx context.Context, userCred mcclient.TokenCredential, parentTaskId string) error { - task, err := taskman.TaskManager.NewTask(ctx, "BaremetalSyncConfigTask", self, userCred, nil, parentTaskId, "", nil) + return self.startSyncConfig(ctx, userCred, parentTaskId, false) +} + +func (self *SHost) startSyncConfig(ctx context.Context, userCred mcclient.TokenCredential, parentTaskId string, noStatus bool) error { + data := jsonutils.NewDict() + data.Add(jsonutils.NewBool(noStatus), "not_sync_status") + task, err := taskman.TaskManager.NewTask(ctx, "BaremetalSyncConfigTask", self, userCred, data, parentTaskId, "", nil) if err != nil { return err } diff --git a/pkg/compute/tasks/baremetal_maintenance_task.go b/pkg/compute/tasks/baremetal_maintenance_task.go index a23ec9a387..fff757deff 100644 --- a/pkg/compute/tasks/baremetal_maintenance_task.go +++ b/pkg/compute/tasks/baremetal_maintenance_task.go @@ -69,12 +69,13 @@ func (self *BaremetalMaintenanceTask) OnEnterMaintenantModeSucc(ctx context.Cont metadatas["__maint_guest_running"] = guestRunning } baremetal.SetAllMetadata(ctx, metadatas, self.UserCred) + baremetal.StartSyncConfig(ctx, self.GetUserCred(), "") self.SetStageComplete(ctx, nil) } func (self *BaremetalMaintenanceTask) OnEnterMaintenantModeSuccFailed(ctx context.Context, baremetal *models.SHost, body jsonutils.JSONObject) { self.SetStageFailed(ctx, body) - baremetal.StartSyncstatus(ctx, self.UserCred, "") + baremetal.StartSyncstatus(ctx, self.GetUserCred(), "") guest := baremetal.GetBaremetalServer() if guest != nil { guest.StartSyncstatus(ctx, self.UserCred, "") diff --git a/pkg/compute/tasks/baremetal_server_sync_status_task.go b/pkg/compute/tasks/baremetal_server_sync_status_task.go index 1c00ecc5b3..1f93cc1d46 100644 --- a/pkg/compute/tasks/baremetal_server_sync_status_task.go +++ b/pkg/compute/tasks/baremetal_server_sync_status_task.go @@ -57,6 +57,7 @@ func (self *BaremetalServerSyncStatusTask) OnInit(ctx context.Context, obj db.IS func (self *BaremetalServerSyncStatusTask) OnGuestStatusTaskComplete(ctx context.Context, guest *models.SGuest, data jsonutils.JSONObject) { var status string var hostStatus string + host := guest.GetHost() if data.Contains("status") { statusStr, _ := data.GetString("status") switch statusStr { @@ -69,6 +70,9 @@ func (self *BaremetalServerSyncStatusTask) OnGuestStatusTaskComplete(ctx context case "admin": status = api.VM_ADMIN hostStatus = api.HOST_STATUS_RUNNING + if !host.IsMaintenance && !host.HasBMC() { + status = api.VM_READY + } default: status = api.VM_INIT hostStatus = api.HOST_STATUS_UNKNOWN @@ -78,7 +82,6 @@ func (self *BaremetalServerSyncStatusTask) OnGuestStatusTaskComplete(ctx context hostStatus = api.HOST_STATUS_UNKNOWN } guest.SetStatus(self.UserCred, status, "BaremetalServerSyncStatusTask") - host := guest.GetHost() host.SetStatus(self.UserCred, hostStatus, "BaremetalServerSyncStatusTask") self.SetStageComplete(ctx, nil) diff --git a/pkg/compute/tasks/baremetal_sync_config_task.go b/pkg/compute/tasks/baremetal_sync_config_task.go index 5f5f238c39..a9412e27a8 100644 --- a/pkg/compute/tasks/baremetal_sync_config_task.go +++ b/pkg/compute/tasks/baremetal_sync_config_task.go @@ -54,9 +54,14 @@ func (self *BaremetalSyncConfigTask) DoSyncConfig(ctx context.Context, baremetal } func (self *BaremetalSyncConfigTask) OnSyncConfigComplete(ctx context.Context, baremetal *models.SHost, body jsonutils.JSONObject) { + logclient.AddActionLogWithStartable(self, baremetal, logclient.ACT_SYNC_CONF, "", self.UserCred, true) + notSyncStatus, _ := self.Params.Bool("not_sync_status") + if notSyncStatus { + self.SetStageComplete(ctx, nil) + return + } self.SetStage("OnSyncstatusComplete", nil) baremetal.StartSyncstatus(ctx, self.UserCred, self.GetTaskId()) - logclient.AddActionLogWithStartable(self, baremetal, logclient.ACT_SYNC_CONF, "", self.UserCred, true) } func (self *BaremetalSyncConfigTask) OnSyncstatusComplete(ctx context.Context, baremetal *models.SHost, body jsonutils.JSONObject) { diff --git a/pkg/compute/tasks/baremetal_sync_status_task.go b/pkg/compute/tasks/baremetal_sync_status_task.go index ad57b28266..8ee65dc983 100644 --- a/pkg/compute/tasks/baremetal_sync_status_task.go +++ b/pkg/compute/tasks/baremetal_sync_status_task.go @@ -56,6 +56,10 @@ func (self *BaremetalSyncStatusTask) OnSyncstatusComplete(ctx context.Context, b self.SetStageComplete(ctx, nil) } +func (self *BaremetalSyncStatusTask) OnSyncstatusCompleteFailed(ctx context.Context, baremetal *models.SHost, body jsonutils.JSONObject) { + self.SetStageFailed(ctx, body) +} + type BaremetalSyncAllGuestsStatusTask struct { SBaremetalBaseTask } @@ -112,6 +116,10 @@ func (self *BaremetalSyncAllGuestsStatusTask) OnGuestSyncStatusComplete(ctx cont self.SetStageComplete(ctx, nil) } +func (self *BaremetalSyncAllGuestsStatusTask) OnGuestSyncStatusCompleteFailed(ctx context.Context, baremetal *models.SHost, body jsonutils.JSONObject) { + self.SetStageFailed(ctx, body) +} + func init() { taskman.RegisterTask(BaremetalSyncStatusTask{}) taskman.RegisterTask(BaremetalSyncAllGuestsStatusTask{}) diff --git a/pkg/compute/tasks/baremetal_unmaintenance_task.go b/pkg/compute/tasks/baremetal_unmaintenance_task.go index 051541c9e6..b8359ad5c9 100644 --- a/pkg/compute/tasks/baremetal_unmaintenance_task.go +++ b/pkg/compute/tasks/baremetal_unmaintenance_task.go @@ -63,10 +63,11 @@ func (self *BaremetalUnmaintenanceTask) OnUnmaintenantComplete(ctx context.Conte "__maint_password": "None", "__maint_ip": "None", } - baremetal.SetAllMetadata(ctx, metadatas, self.UserCred) + baremetal.SetAllMetadata(ctx, metadatas, self.GetUserCred()) self.SetStageComplete(ctx, nil) guest := baremetal.GetBaremetalServer() if guest != nil { - guest.StartSyncstatus(ctx, self.UserCred, "") + guest.StartSyncstatus(ctx, self.GetUserCred(), "") } + baremetal.StartSyncConfig(ctx, self.GetUserCred(), "") } diff --git a/pkg/hostman/guestfs/fsdriver/linux.go b/pkg/hostman/guestfs/fsdriver/linux.go index 7db13e3c18..a714aad2ab 100644 --- a/pkg/hostman/guestfs/fsdriver/linux.go +++ b/pkg/hostman/guestfs/fsdriver/linux.go @@ -596,6 +596,10 @@ func (d *sDebianLikeRootFs) DeployNetworkingScripts(rootFs IDiskPartition, nics if mainNic != nil { mainIp = mainNic.Ip } + + var systemdResolveConfig strings.Builder + dnss := []string{} + domains := []string{} for i := range allNics { nicDesc := allNics[i] cmds.WriteString(fmt.Sprintf("auto %s\n", nicDesc.Name)) @@ -628,8 +632,10 @@ func (d *sDebianLikeRootFs) DeployNetworkingScripts(rootFs IDiskPartition, nics dnslist := netutils2.GetNicDns(nicDesc) if len(dnslist) > 0 { cmds.WriteString(fmt.Sprintf(" dns-nameservers %s\n", strings.Join(dnslist, " "))) + dnss = append(dnss, dnslist...) if len(nicDesc.Domain) > 0 { cmds.WriteString(fmt.Sprintf(" dns-search %s\n", nicDesc.Domain)) + domains = append(domains, nicDesc.Domain) } } if len(nicDesc.TeamingSlaves) > 0 { @@ -644,6 +650,19 @@ func (d *sDebianLikeRootFs) DeployNetworkingScripts(rootFs IDiskPartition, nics cmds.WriteString("\n") } } + + if len(dnss) != 0 { + systemdResolveConfig.WriteString("[Resolve]\n") + systemdResolveConfig.WriteString(fmt.Sprintf("DNS=%s\n", strings.Join(dnss, " "))) + if len(domains) != 0 { + systemdResolveConfig.WriteString(fmt.Sprintf("Domains=%s\n", strings.Join(domains, " "))) + } + systemdResolveFn := "/etc/systemd/resolved.conf" + content := systemdResolveConfig.String() + if err := rootFs.FilePutContents(systemdResolveFn, content, false, false); err != nil { + log.Warningf("Put %s to %s error: %v", content, systemdResolveFn, err) + } + } log.Debugf("%s", cmds.String()) return rootFs.FilePutContents(fn, cmds.String(), false, false) } diff --git a/pkg/util/ssh/ssh.go b/pkg/util/ssh/ssh.go index a5ccd01848..176ea660bc 100644 --- a/pkg/util/ssh/ssh.go +++ b/pkg/util/ssh/ssh.go @@ -144,18 +144,23 @@ func (s *Client) GetConfig() ClientConfig { } func (s *Client) RawRun(cmds ...string) ([]string, error) { - return s.run(false, cmds, nil) + return s.run(false, cmds, nil, false) } func (s *Client) Run(cmds ...string) ([]string, error) { - return s.run(true, cmds, nil) + return s.run(true, cmds, nil, false) } func (s *Client) RunWithInput(input io.Reader, cmds ...string) ([]string, error) { - return s.run(true, cmds, input) + return s.run(true, cmds, input, false) } -func (s *Client) run(parseOutput bool, cmds []string, input io.Reader) ([]string, error) { +// RunWithTTY request Pty before run command. +func (s *Client) RunWithTTY(cmds ...string) ([]string, error) { + return s.run(false, cmds, nil, true) +} + +func (s *Client) run(parseOutput bool, cmds []string, input io.Reader, withPty bool) ([]string, error) { ret := []string{} for _, cmd := range cmds { session, err := s.client.NewSession() @@ -163,6 +168,18 @@ func (s *Client) run(parseOutput bool, cmds []string, input io.Reader) ([]string return nil, err } defer session.Close() + + if withPty { + modes := ssh.TerminalModes{ + ssh.ECHO: 1, // enable echoing + ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud + ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud + } + if err := session.RequestPty("xterm", 24, 80, modes); err != nil { + return nil, errors.Wrap(err, "Setup TTY") + } + } + log.Debugf("Run command: %s", cmd) var stdOut bytes.Buffer var stdErr bytes.Buffer @@ -255,3 +272,11 @@ func (s *Client) RunTerminal() error { } return nil } + +func IsExitMissingError(err error) bool { + errStr := new(ssh.ExitMissingError).Error() + if strings.Contains(err.Error(), errStr) { + return true + } + return false +}