diff --git a/pkg/baremetal/manager.go b/pkg/baremetal/manager.go index a14ccda9aa..3c06748d5a 100644 --- a/pkg/baremetal/manager.go +++ b/pkg/baremetal/manager.go @@ -999,9 +999,12 @@ func (b *SBaremetalInstance) getDHCPConfig( if err != nil { return nil, err } - if isPxe && !b.NeedPXEBoot() { + 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 PXE boot", b.GetName()) + return nil, errors.Errorf("Baremetal %s not need UEFI PXE boot", b.GetName()) } return GetNicDHCPConfig(nic, serverIP.String(), hostName, isPxe, arch) } 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/tasks/base.go b/pkg/baremetal/tasks/base.go index 9a8983ea57..6e76dea961 100644 --- a/pkg/baremetal/tasks/base.go +++ b/pkg/baremetal/tasks/base.go @@ -26,6 +26,7 @@ import ( "yunion.io/x/pkg/errors" o "yunion.io/x/onecloud/pkg/baremetal/options" + "yunion.io/x/onecloud/pkg/baremetal/utils/uefi" "yunion.io/x/onecloud/pkg/cloudcommon/object" "yunion.io/x/onecloud/pkg/cloudcommon/types" "yunion.io/x/onecloud/pkg/mcclient" @@ -490,3 +491,31 @@ func (self *SBaremetalPXEBootTaskBase) OnStopComplete(ctx context.Context, args func (self *SBaremetalPXEBootTaskBase) GetName() string { return "BaremetalPXEBootTaskBase" } + +func AdjustUEFIBootOrder(term *ssh.Client) error { + isUEFI, err := uefi.RemoteIsUEFIBoot(term) + if err != nil { + return errors.Wrap(err, "Check baremetal is UEFI boot") + } + + if !isUEFI { + return nil + } + + mgr, err := uefi.NewEFIBootMgrFromRemote(term) + if err != nil { + return errors.Wrap(err, "NewEFIBootMgrFromRemote") + } + + log.Errorf("=====before set ") + time.Sleep(30 * time.Second) + + if err := uefi.RemoteSetCurrentBootAtFirst(term, mgr); err != nil { + return errors.Wrap(err, "Set current pxe boot at fist") + } + + log.Errorf("=====after set ") + time.Sleep(30 * time.Second) + + return nil +} diff --git a/pkg/baremetal/tasks/basedeploy.go b/pkg/baremetal/tasks/basedeploy.go index 725c7edcd4..3d36865fc6 100644 --- a/pkg/baremetal/tasks/basedeploy.go +++ b/pkg/baremetal/tasks/basedeploy.go @@ -89,6 +89,11 @@ func (self *SBaremetalServerBaseDeployTask) OnPXEBoot(ctx context.Context, term if err != nil { return errors.Wrap(err, "Do deploy") } + + if err := AdjustUEFIBootOrder(term); err != nil { + return errors.Wrap(err, "Adjust UEFI boot order") + } + _, err = term.Run( "/bin/sync", "/sbin/sysctl -w vm.drop_caches=3", diff --git a/pkg/baremetal/tasks/baseprepare.go b/pkg/baremetal/tasks/baseprepare.go index b5e8f69dee..25da5d1c73 100644 --- a/pkg/baremetal/tasks/baseprepare.go +++ b/pkg/baremetal/tasks/baseprepare.go @@ -327,6 +327,11 @@ func (task *sBaremetalPrepareTask) DoPrepare(cli *ssh.Client) error { log.Errorf("SetNTP fail: %s", err) } + if err = AdjustUEFIBootOrder(cli); 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") diff --git a/pkg/baremetal/utils/uefi/uefi.go b/pkg/baremetal/utils/uefi/uefi.go new file mode 100644 index 0000000000..f09b5a19c0 --- /dev/null +++ b/pkg/baremetal/utils/uefi/uefi.go @@ -0,0 +1,324 @@ +// 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/util/regutils2" + "yunion.io/x/onecloud/pkg/util/ssh" +) + +const ( + // efibootmgr useage: https://github.com/rhboot/efibootmgr + CMD_EFIBOOTMGR = "/usr/sbin/efibootmgr" +) + +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 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) (*BootMgr, error) { + cmd := CMD_EFIBOOTMGR + 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() string { + return CMD_EFIBOOTMGR +} + +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) 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 (m *BootMgr) GetSetBootOrderArgs() string { + return strings.Join(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 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(), mgr.GetSetBootOrderArgs()) + _, err := cli.Run(cmd) + return err +} 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) + } + }) + } +}