feat(baremetal): add UEFI related util

This commit is contained in:
Zexi Li
2021-06-03 17:28:23 +08:00
parent 3f87a6fc6f
commit ef7bc9e09d
7 changed files with 710 additions and 4 deletions

View File

@@ -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)
}

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -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",

View File

@@ -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")

View File

@@ -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<seconds>[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<num>[0-9a-zA-Z]{4})[^:]+?\s+(?P<description>.*)`
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
}

View File

@@ -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)
}
})
}
}