Skip to content

Commit

Permalink
feat: add new grub parser and descriptive grub menu entries
Browse files Browse the repository at this point in the history
Rewrite the grub config parser code, allow to have descriptive Grub entries.
Remove old syslinux bootloader.

Fixes #4914

Signed-off-by: Utku Ozdemir <[email protected]>
  • Loading branch information
utkuozdemir authored and smira committed Feb 18, 2022
1 parent 6ccfdba commit 4d5cd66
Show file tree
Hide file tree
Showing 18 changed files with 737 additions and 703 deletions.
74 changes: 41 additions & 33 deletions cmd/installer/pkg/install/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,26 +83,22 @@ type Installer struct {

bootPartitionFound bool

Current string
Next string
Current grub.BootLabel
Next grub.BootLabel
}

// NewInstaller initializes and returns an Installer.
func NewInstaller(cmdline *procfs.Cmdline, seq runtime.Sequence, opts *Options) (i *Installer, err error) {
i = &Installer{
cmdline: cmdline,
options: opts,
bootloader: &grub.Grub{
BootDisk: opts.Disk,
Arch: opts.Arch,
},
}

if err = i.probeBootPartition(); err != nil {
return nil, err
}

i.manifest, err = NewManifest(i.Next, seq, i.bootPartitionFound, i.options)
i.manifest, err = NewManifest(string(i.Next), seq, i.bootPartitionFound, i.options)
if err != nil {
return nil, fmt.Errorf("failed to create installation manifest: %w", err)
}
Expand Down Expand Up @@ -152,10 +148,25 @@ func (i *Installer) probeBootPartition() error {
}
}

var err error
grubConf, err := grub.Read(grub.ConfigPath)
if err != nil {
return err
}

// anyways run the Labels() to get the defaults initialized
i.Current, i.Next, err = i.bootloader.Labels()
next := grub.BootA

if grubConf != nil {
i.Current = grubConf.Default

next, err = grub.FlipBootLabel(grubConf.Default)
if err != nil {
return err
}

i.bootloader = grubConf
}

i.Next = next

return err
}
Expand Down Expand Up @@ -262,32 +273,29 @@ func (i *Installer) Install(seq runtime.Sequence) (err error) {
return nil
}

i.cmdline.Append("initrd", filepath.Join("/", i.Next, constants.InitramfsAsset))

grubcfg := &grub.Cfg{
Default: i.Next,
Labels: []*grub.Label{
{
Root: i.Next,
Initrd: filepath.Join("/", i.Next, constants.InitramfsAsset),
Kernel: filepath.Join("/", i.Next, constants.KernelAsset),
Append: i.cmdline.String(),
},
},
}
i.cmdline.Append("initrd", filepath.Join("/", string(i.Next), constants.InitramfsAsset))

if i.Current != "" {
grubcfg.Fallback = i.Current
var conf *grub.Config
if i.bootloader == nil {
conf = grub.NewConfig(i.cmdline.String())
} else {
existingConf, ok := i.bootloader.(*grub.Config)
if !ok {
return fmt.Errorf("unsupported bootloader type: %T", i.bootloader)
}
if err = existingConf.Put(i.Next, i.cmdline.String()); err != nil {
return err
}
existingConf.Default = i.Next
existingConf.Fallback = i.Current

grubcfg.Labels = append(grubcfg.Labels, &grub.Label{
Root: i.Current,
Initrd: filepath.Join("/", i.Current, constants.InitramfsAsset),
Kernel: filepath.Join("/", i.Current, constants.KernelAsset),
Append: procfs.ProcCmdline().String(),
})
conf = existingConf
}

if err = i.bootloader.Install(i.Current, grubcfg, seq); err != nil {
i.bootloader = conf

err = i.bootloader.Install(i.options.Disk, i.options.Arch)
if err != nil {
return err
}

Expand Down Expand Up @@ -316,7 +324,7 @@ func (i *Installer) Install(seq runtime.Sequence) (err error) {
//nolint:errcheck
defer meta.Close()

if ok := meta.LegacyADV.SetTag(adv.Upgrade, i.Current); !ok {
if ok := meta.LegacyADV.SetTag(adv.Upgrade, string(i.Current)); !ok {
return fmt.Errorf("failed to set upgrade tag: %q", i.Current)
}

Expand Down
17 changes: 12 additions & 5 deletions internal/app/machined/internal/server/v1alpha1/v1alpha1_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -289,20 +289,27 @@ func (s *Server) Rollback(ctx context.Context, in *machine.RollbackRequest) (*ma
return fmt.Errorf("boot disk not found")
}

grub := &grub.Grub{
BootDisk: disk.Device().Name(),
conf, err := grub.Read(grub.ConfigPath)
if err != nil {
return err
}

if conf == nil {
return fmt.Errorf("grub configuration not found, nothing to rollback")
}

_, next, err := grub.Labels()
next, err := grub.FlipBootLabel(conf.Default)
if err != nil {
return err
}

if _, err = os.Stat(filepath.Join(constants.BootMountPoint, next)); errors.Is(err, os.ErrNotExist) {
if _, err = os.Stat(filepath.Join(constants.BootMountPoint, string(next))); errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("cannot rollback to %q, label does not exist", next)
}

if err := grub.Default(next); err != nil {
conf.Default = next
conf.Fallback = ""
if err := conf.Write(grub.ConfigPath); err != nil {
return fmt.Errorf("failed to revert bootloader: %v", err)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,8 @@

package bootloader

import (
"github.com/talos-systems/talos/internal/app/machined/pkg/runtime"
)

// Bootloader describes a bootloader.
type Bootloader interface {
Labels() (string, string, error)
Install(string, interface{}, runtime.Sequence) error
Default(string) error
// Install installs the bootloader
Install(bootDisk, arch string) error
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

package grub

import (
"fmt"
"strings"
)

// BootLabel represents a boot label, e.g. A or B.
type BootLabel string

// FlipBootLabel flips the boot entry, e.g. A -> B, B -> A.
func FlipBootLabel(e BootLabel) (BootLabel, error) {
switch e {
case BootA:
return BootB, nil
case BootB:
return BootA, nil
default:
return "", fmt.Errorf("invalid entry: %s", e)
}
}

// ParseBootLabel parses the given human-readable boot label to a grub.BootLabel.
func ParseBootLabel(name string) (BootLabel, error) {
if strings.HasPrefix(name, string(BootA)) {
return BootA, nil
}

if strings.HasPrefix(name, string(BootB)) {
return BootB, nil
}

return "", fmt.Errorf("could not parse boot entry from name: %s", name)
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,11 @@ import "github.com/talos-systems/talos/pkg/machinery/constants"

const (
// BootA is a bootloader label.
BootA = "A"
BootA BootLabel = "A"

// BootB is a bootloader label.
BootB = "B"
BootB BootLabel = "B"

// GrubConfig is the path to the grub config.
GrubConfig = constants.BootMountPoint + "/grub/grub.cfg"

// GrubDeviceMap is the path to the grub device map.
GrubDeviceMap = constants.BootMountPoint + "/grub/device.map"
// ConfigPath is the path to the grub config.
ConfigPath = constants.BootMountPoint + "/grub/grub.cfg"
)
150 changes: 150 additions & 0 deletions internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/decode.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

package grub

import (
"errors"
"fmt"
"io/ioutil"
"os"
"regexp"
)

var (
defaultEntryRegex = regexp.MustCompile(`(?m)^\s*set default="(.*)"\s*$`)
fallbackEntryRegex = regexp.MustCompile(`(?m)^\s*set fallback="(.*)"\s*$`)
menuEntryRegex = regexp.MustCompile(`(?m)^menuentry "(.+)" {([^}]+)}`)
linuxRegex = regexp.MustCompile(`(?m)^\s*linux\s+(.+?)\s+(.*)$`)
initrdRegex = regexp.MustCompile(`(?m)^\s*initrd\s+(.+)$`)
)

// Read reads the grub configuration from the disk.
func Read(path string) (*Config, error) {
c, err := ioutil.ReadFile(path)
if errors.Is(err, os.ErrNotExist) {
return nil, nil
}

if err != nil {
return nil, err
}

return Decode(c)
}

// Decode parses the grub configuration from the given bytes.
func Decode(c []byte) (*Config, error) {
defaultEntryMatches := defaultEntryRegex.FindAllSubmatch(c, -1)
if len(defaultEntryMatches) != 1 {
return nil, fmt.Errorf("failed to find default")
}

fallbackEntryMatches := fallbackEntryRegex.FindAllSubmatch(c, -1)
if len(fallbackEntryMatches) > 1 {
return nil, fmt.Errorf("found multiple fallback entries")
}

var fallbackEntry BootLabel

if len(fallbackEntryMatches) == 1 {
if len(fallbackEntryMatches[0]) != 2 {
return nil, fmt.Errorf("failed to parse fallback entry")
}

entry, err := ParseBootLabel(string(fallbackEntryMatches[0][1]))
if err != nil {
return nil, err
}

fallbackEntry = entry
}

if len(defaultEntryMatches[0]) != 2 {
return nil, fmt.Errorf("expected 2 matches, got %d", len(defaultEntryMatches[0]))
}

defaultEntry, err := ParseBootLabel(string(defaultEntryMatches[0][1]))
if err != nil {
return nil, err
}

entries, err := parseEntries(c)
if err != nil {
return nil, err
}

conf := Config{
Default: defaultEntry,
Fallback: fallbackEntry,
Entries: entries,
}

return &conf, nil
}

func parseEntries(conf []byte) (map[BootLabel]MenuEntry, error) {
entries := make(map[BootLabel]MenuEntry)

matches := menuEntryRegex.FindAllSubmatch(conf, -1)
for _, m := range matches {
if len(m) != 3 {
return nil, fmt.Errorf("expected 3 matches, got %d", len(m))
}

confBlock := m[2]

linux, cmdline, initrd, err := parseConfBlock(confBlock)
if err != nil {
return nil, err
}

name := string(m[1])

bootEntry, err := ParseBootLabel(name)
if err != nil {
return nil, err
}

entries[bootEntry] = MenuEntry{
Name: name,
Linux: linux,
Cmdline: cmdline,
Initrd: initrd,
}
}

return entries, nil
}

func parseConfBlock(block []byte) (linux, cmdline, initrd string, err error) {
linuxMatches := linuxRegex.FindAllSubmatch(block, -1)
if len(linuxMatches) != 1 {
return "", "", "",
fmt.Errorf("expected 1 match, got %d", len(linuxMatches))
}

if len(linuxMatches[0]) != 3 {
return "", "", "",
fmt.Errorf("expected 3 matches, got %d", len(linuxMatches[0]))
}

linux = string(linuxMatches[0][1])
cmdline = string(linuxMatches[0][2])

initrdMatches := initrdRegex.FindAllSubmatch(block, -1)
if len(initrdMatches) != 1 {
return "", "", "",
fmt.Errorf("expected 1 match, got %d", len(initrdMatches))
}

if len(initrdMatches[0]) != 2 {
return "", "", "",
fmt.Errorf("expected 2 matches, got %d", len(initrdMatches[0]))
}

initrd = string(initrdMatches[0][1])

return linux, cmdline, initrd, nil
}
Loading

0 comments on commit 4d5cd66

Please sign in to comment.