From 1383272d2c12b9fce70793861dd5f261bc937194 Mon Sep 17 00:00:00 2001 From: Melora Hugues Date: Sun, 13 Aug 2023 17:59:05 +0200 Subject: [PATCH] Add getting efi info from efibootmgr --- config/go.mod | 8 +++ config/go.sum | 10 ++++ config/logger/logger.go | 31 ++++++++--- config/main.go | 54 ++++++++++++++++--- config/prober/prober.go | 102 +++++++++++++++++++++++++++++++++++ config/prober/prober_test.go | 46 ++++++++++++++++ 6 files changed, 236 insertions(+), 15 deletions(-) create mode 100644 config/go.sum create mode 100644 config/prober/prober.go create mode 100644 config/prober/prober_test.go diff --git a/config/go.mod b/config/go.mod index 598eec5..bafeacd 100644 --- a/config/go.mod +++ b/config/go.mod @@ -1,3 +1,11 @@ module git.faercol.me/faercol/http-boot-config/config go 1.20 + +require github.com/stretchr/testify v1.8.4 + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/config/go.sum b/config/go.sum new file mode 100644 index 0000000..fa4b6e6 --- /dev/null +++ b/config/go.sum @@ -0,0 +1,10 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/config/logger/logger.go b/config/logger/logger.go index 8b0c335..cd5bb11 100644 --- a/config/logger/logger.go +++ b/config/logger/logger.go @@ -7,19 +7,22 @@ import ( ) const ( - NC = "\033[0m" + nc = "\033[0m" red = "\033[0;31m" yellow = "\033[0;33m" + gray = "\033[0;30m" ) const ( errorPrefix = "Error: " fatalPrefix = "Fatal: " warningprefix = "Warning: " + debugprefix = "Debug: " ) type SimpleLogger struct { enableColour bool + debug bool } func (sl *SimpleLogger) printMsg(msg string, dest io.Writer) { @@ -40,7 +43,7 @@ func (sl *SimpleLogger) colourText(msg string, colour string) string { if !sl.enableColour { return msg } - return colour + msg + NC + return colour + msg + nc } func (sl *SimpleLogger) Info(msg string) { @@ -67,6 +70,19 @@ func (sl *SimpleLogger) Warningf(format string, a ...any) { sl.printMsg(sl.colourText(warningprefix+fmt.Sprintf(format, a...), yellow), os.Stdout) } +func (sl *SimpleLogger) Debug(msg string) { + if !sl.debug { + return + } + sl.printMsg(sl.colourText(debugprefix+msg, gray), os.Stdout) +} + +func (sl *SimpleLogger) Debugf(format string, a ...any) { + if !sl.debug { + return + } + sl.printMsg(sl.colourText(debugprefix+fmt.Sprintf(format, a...), gray), os.Stdout) +} func (sl *SimpleLogger) Fatal(msg string) { sl.printMsg(sl.colourText(fatalPrefix+msg, red), os.Stderr) os.Exit(1) @@ -77,10 +93,9 @@ func (sl *SimpleLogger) Fatalf(format string, a ...any) { os.Exit(1) } -func WithColour() *SimpleLogger { - return &SimpleLogger{enableColour: true} -} - -func NoColour() *SimpleLogger { - return &SimpleLogger{enableColour: false} +func New(colour, debug bool) *SimpleLogger { + return &SimpleLogger{ + debug: debug, + enableColour: colour, + } } diff --git a/config/main.go b/config/main.go index 8ab1cf9..201913e 100644 --- a/config/main.go +++ b/config/main.go @@ -1,14 +1,54 @@ package main -import "git.faercol.me/faercol/http-boot-config/config/logger" +import ( + "errors" + "flag" + "io/fs" + + "git.faercol.me/faercol/http-boot-config/config/logger" + "git.faercol.me/faercol/http-boot-config/config/prober" +) + +type cliArgs struct { + debug bool + colour bool +} + +func parseArgs() cliArgs { + debugFlag := flag.Bool("debug", false, "Display debug logs") + noColourFlag := flag.Bool("no-colour", false, "Disable colour logs") + + flag.Parse() + + return cliArgs{ + debug: *debugFlag, + colour: !*noColourFlag, + } +} + +func displayAppList(apps []prober.EfiApp, l *logger.SimpleLogger) { + l.Info("Found the following EFI applications:") + for _, a := range apps { + prefix := " " + if a.Active { + prefix = "*" + } + l.Infof("\t- %s[%d] %s: %s", prefix, a.ID, a.Name, a.Path) + } +} func main() { - l := logger.NoColour() + args := parseArgs() + l := logger.New(args.colour, args.debug) - l.Info("test message") - l.Warning("this is a warning") - l.Error("this is an error") + l.Info("Checking EFI directory for available boot images...") + images, err := prober.GetEFIApps(l) + if err != nil { + if errors.Is(err, fs.ErrPermission) { + l.Fatal("Permission error, try to run the command as sudo") + } + l.Fatalf("Failed to check EFI directory: %s", err.Error()) + } - l.Fatal("this is fatal") - l.Info("This should not be displayed") + displayAppList(images, l) } diff --git a/config/prober/prober.go b/config/prober/prober.go new file mode 100644 index 0000000..bab5524 --- /dev/null +++ b/config/prober/prober.go @@ -0,0 +1,102 @@ +package prober + +import ( + "bytes" + "errors" + "fmt" + "os/exec" + "regexp" + "strconv" + "strings" + + "git.faercol.me/faercol/http-boot-config/config/logger" +) + +var efiBootmgrRegexp = regexp.MustCompile(`Boot(?P\d+)\* (?P[\w ]+)\t(\w+\(.*\))/File\((?P.+)\)`) +var activeRegexp = regexp.MustCompile(`BootCurrent: (\d+)`) + +type EfiApp struct { + ID int + Name string + Path string + Active bool +} + +func efiAppFromBootMgrOutput(rawVal string, activeId int) (app EfiApp, ok bool) { + match := efiBootmgrRegexp.FindStringSubmatch(rawVal) + if len(match) == 0 { + return + } + result := make(map[string]string) + for i, name := range efiBootmgrRegexp.SubexpNames() { + if i != 0 && name != "" { + result[name] = match[i] + } + } + id, ok := result["id"] + if !ok { + return + } + idInt, err := strconv.Atoi(id) + if err != nil { + return app, false + } + name, ok := result["name"] + if !ok { + return + } + filepath, ok := result["filepath"] + if !ok { + return + } + return EfiApp{ + ID: idInt, + Name: strings.TrimSpace(name), + Path: strings.TrimSpace(filepath), + Active: idInt == activeId, + }, true +} + +func getActiveEFIApp(output string) (int, error) { + match := activeRegexp.FindStringSubmatch(output) + if len(match) == 0 { + return -1, errors.New("no active boot image found") + } + strId := match[1] + id, err := strconv.Atoi(strId) + if err != nil { + return -1, err + } + return id, nil +} + +func GetEFIApps(log *logger.SimpleLogger) (apps []EfiApp, err error) { + cmd := exec.Command("/usr/bin/efibootmgr") + var out bytes.Buffer + cmd.Stdout = &out + + err = cmd.Run() + if err != nil { + return + } + if cmd.ProcessState.ExitCode() != 0 { + return nil, fmt.Errorf("error running efibootmgr, returncode %d", cmd.ProcessState.ExitCode()) + } + + outStr := out.String() + activeBootID, err := getActiveEFIApp(outStr) + if err != nil { + return + } + + for _, l := range strings.Split(outStr, "\n") { + log.Debugf("Parsing line %q", l) + app, ok := efiAppFromBootMgrOutput(l, activeBootID) + if !ok { + log.Debug("Line is not a valid EFI application") + continue + } + apps = append(apps, app) + } + return +} diff --git a/config/prober/prober_test.go b/config/prober/prober_test.go new file mode 100644 index 0000000..7e4a97a --- /dev/null +++ b/config/prober/prober_test.go @@ -0,0 +1,46 @@ +package prober + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestEfiFomBootMgr(t *testing.T) { + t.Run("OK - Windows", func(t *testing.T) { + rawVal := (`Boot0000* Windows Boot Manager HD(1,GPT,4ad714ba-6a01-4f76-90e3-1bb93a59b67e,0x800,0x32000)/File(\EFI\MICROSOFT\BOOT\BOOTMGFW.EFI)` + + `57494e444f5753000100000088000000780000004200430044004f0042004a004500430054003d007b00390064006500610038003600320063002d0035006300640064002d00`) + expected := EfiApp{ID: 0, Name: "Windows Boot Manager", Path: `\EFI\MICROSOFT\BOOT\BOOTMGFW.EFI`, Active: true} + app, ok := efiAppFromBootMgrOutput(rawVal, 0) + assert.True(t, ok) + assert.Equal(t, expected, app) + }) + + t.Run("OK - Manjaro", func(t *testing.T) { + rawVal := `Boot0001* Manjaro HD(1,GPT,16f06d01-50da-6544-86bd-f3457f980086,0x1000,0x96000)/File(\EFI\MANJARO\GRUBX64.EFI)` + expected := EfiApp{ID: 1, Name: "Manjaro", Path: `\EFI\MANJARO\GRUBX64.EFI`, Active: false} + app, ok := efiAppFromBootMgrOutput(rawVal, 0) + assert.True(t, ok) + assert.Equal(t, expected, app) + }) + + t.Run("OK - EFI Base", func(t *testing.T) { + rawVal := `Boot0002* UEFI OS HD(1,GPT,16f06d01-50da-6544-86bd-f3457f980086,0x1000,0x96000)/File(\EFI\BOOT\BOOTX64.EFI)0000424f` + expected := EfiApp{ID: 2, Name: "UEFI OS", Path: `\EFI\BOOT\BOOTX64.EFI`, Active: false} + app, ok := efiAppFromBootMgrOutput(rawVal, 0) + assert.True(t, ok) + assert.Equal(t, expected, app) + }) + + t.Run("NOK - CD", func(t *testing.T) { + rawVal := `Boot0003* UEFI:CD/DVD Drive BBS(129,,0x0)` + _, ok := efiAppFromBootMgrOutput(rawVal, 0) + assert.False(t, ok) + }) + + t.Run("NOK - Other line", func(t *testing.T) { + rawVal := `BootOrder: 0001,0002,0000,0003,0004,0005` + _, ok := efiAppFromBootMgrOutput(rawVal, 0) + assert.False(t, ok) + }) +}