feat: add new grub parser and descriptive grub menu entries

Rewrite the grub config parser code, allow to have descriptive Grub entries.
Remove old syslinux bootloader.

Fixes talos-systems/talos#4914

Signed-off-by: Utku Ozdemir <uoz@protonmail.com>
This commit is contained in:
Utku Ozdemir 2022-02-09 21:11:42 +01:00 committed by Andrey Smirnov
parent 6ccfdbaf1b
commit 4d5cd66538
No known key found for this signature in database
GPG Key ID: 7B26396447AB6DFD
18 changed files with 743 additions and 709 deletions

View File

@ -83,8 +83,8 @@ type Installer struct {
bootPartitionFound bool
Current string
Next string
Current grub.BootLabel
Next grub.BootLabel
}
// NewInstaller initializes and returns an Installer.
@ -92,17 +92,13 @@ func NewInstaller(cmdline *procfs.Cmdline, seq runtime.Sequence, opts *Options)
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)
}
@ -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
}
@ -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))
i.cmdline.Append("initrd", filepath.Join("/", string(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(),
},
},
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
conf = existingConf
}
if i.Current != "" {
grubcfg.Fallback = i.Current
i.bootloader = conf
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(),
})
}
if err = i.bootloader.Install(i.Current, grubcfg, seq); err != nil {
err = i.bootloader.Install(i.options.Disk, i.options.Arch)
if err != nil {
return err
}
@ -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)
}

View File

@ -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(),
}
_, next, err := grub.Labels()
conf, err := grub.Read(grub.ConfigPath)
if err != nil {
return err
}
if _, err = os.Stat(filepath.Join(constants.BootMountPoint, next)); errors.Is(err, os.ErrNotExist) {
if conf == nil {
return fmt.Errorf("grub configuration not found, nothing to rollback")
}
next, err := grub.FlipBootLabel(conf.Default)
if err != nil {
return err
}
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)
}

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,66 @@
// 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 (
"bytes"
"io"
"io/ioutil"
"log"
"os"
"path/filepath"
"text/template"
)
const confTemplate = `set default="{{ (index .Entries .Default).Name }}"
{{ with (index .Entries .Fallback).Name -}}
set fallback="{{ . }}"
{{- end }}
set timeout=3
insmod all_video
terminal_input console
terminal_output console
{{ range $key, $entry := .Entries -}}
menuentry "{{ $entry.Name }}" {
set gfxmode=auto
set gfxpayload=text
linux {{ $entry.Linux }} {{ $entry.Cmdline }}
initrd {{ $entry.Initrd }}
}
{{ end -}}
`
// Write the grub configuration to the given file.
func (c *Config) Write(path string) error {
dir := filepath.Dir(path)
if err := os.MkdirAll(dir, os.ModeDir); err != nil {
return err
}
wr := new(bytes.Buffer)
err := c.Encode(wr)
if err != nil {
return err
}
log.Printf("writing %s to disk", path)
return ioutil.WriteFile(path, wr.Bytes(), 0o600)
}
// Encode writes the grub configuration to the given writer.
func (c *Config) Encode(wr io.Writer) error {
if err := c.validate(); err != nil {
return err
}
t := template.Must(template.New("grub").Parse(confTemplate))
return t.Execute(wr, c)
}

View File

@ -2,295 +2,72 @@
// 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 provides the interface to the GRUB bootloader: config management, installation, etc.
package grub
import (
"bufio"
"bytes"
"errors"
"fmt"
"io/ioutil"
"log"
"os"
"os/exec"
"path/filepath"
"regexp"
goruntime "runtime"
"strings"
"text/template"
"github.com/talos-systems/go-blockdevice/blockdevice"
"github.com/talos-systems/talos/internal/app/machined/pkg/runtime"
"github.com/talos-systems/talos/pkg/machinery/constants"
"github.com/talos-systems/talos/pkg/version"
)
// Cfg reprsents the cfg file.
type Cfg struct {
Default string
Fallback string
Labels []*Label
// Config represents a grub configuration file (grub.cfg).
type Config struct {
Default BootLabel
Fallback BootLabel
Entries map[BootLabel]MenuEntry
}
// Label reprsents a label in the cfg file.
type Label struct {
Root string
Kernel string
Initrd string
Append string
}
const grubCfgTpl = `set default="{{ .Default }}"
{{ with .Fallback -}}
set fallback="{{ . }}"
{{- end }}
set timeout=3
insmod all_video
terminal_input console
terminal_output console
{{ range $label := .Labels -}}
menuentry "{{ $label.Root }}" {
set gfxmode=auto
set gfxpayload=text
linux {{ $label.Kernel }} {{ $label.Append }}
initrd {{ $label.Initrd }}
}
{{ end }}
`
const (
amd64 = "amd64"
arm64 = "arm64"
)
// Grub represents the grub bootloader.
type Grub struct {
BootDisk string
Arch string
}
// Labels implements the Bootloader interface.
func (g *Grub) Labels() (current, next string, err error) {
var b []byte
if b, err = ioutil.ReadFile(GrubConfig); err != nil {
if errors.Is(err, os.ErrNotExist) {
next = BootA
return current, next, nil
}
return "", "", err
}
re := regexp.MustCompile(`^set default="(.*)"`)
matches := re.FindAllSubmatch(b, -1)
if len(matches) != 1 {
return "", "", fmt.Errorf("failed to find default")
}
if len(matches[0]) != 2 {
return "", "", fmt.Errorf("expected 2 matches, got %d", len(matches[0]))
}
current = string(matches[0][1])
switch current {
case BootA:
next = BootB
case BootB:
next = BootA
default:
return "", "", fmt.Errorf("unknown grub menuentry: %q", current)
}
return current, next, err
}
// BootEntry describes GRUB boot entry.
type BootEntry struct {
// Paths to kernel and initramfs image.
Linux, Initrd string
// Cmdline for the kernel.
// MenuEntry represents a grub menu entry in the grub config file.
type MenuEntry struct {
Name string
Linux string
Cmdline string
Initrd string
}
// GetCurrentEntry fetches current boot entry, vmlinuz/initrd path, boot args.
//
//nolint:gocyclo
func (g *Grub) GetCurrentEntry() (*BootEntry, error) {
f, err := os.Open(GrubConfig)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil, nil
}
return nil, err
// NewConfig creates a new grub configuration (nothing is written to disk).
func NewConfig(cmdline string) *Config {
return &Config{
Default: BootA,
Entries: map[BootLabel]MenuEntry{
BootA: buildMenuEntry(BootA, cmdline),
},
}
defer f.Close() //nolint:errcheck
scanner := bufio.NewScanner(f)
entry := &BootEntry{}
var (
defaultEntry string
currentEntry string
)
for scanner.Scan() {
line := scanner.Text()
switch {
case strings.HasPrefix(line, "set default"):
matches := regexp.MustCompile(`set default="(.*)"`).FindStringSubmatch(line)
if len(matches) != 2 {
return nil, fmt.Errorf("malformed default entry: %q", line)
}
defaultEntry = matches[1]
case strings.HasPrefix(line, "menuentry"):
matches := regexp.MustCompile(`menuentry "(.*)"`).FindStringSubmatch(line)
if len(matches) != 2 {
return nil, fmt.Errorf("malformed menuentry: %q", line)
}
currentEntry = matches[1]
case strings.HasPrefix(line, " linux "):
if currentEntry != defaultEntry {
continue
}
parts := strings.SplitN(line[8:], " ", 2)
entry.Linux = parts[0]
if len(parts) == 2 {
entry.Cmdline = parts[1]
}
case strings.HasPrefix(line, " initrd "):
if currentEntry != defaultEntry {
continue
}
entry.Initrd = line[9:]
}
}
if entry.Linux == "" || entry.Initrd == "" {
return nil, scanner.Err()
}
return entry, scanner.Err()
}
// Install implements the Bootloader interface. It sets up grub with the
// specified kernel parameters.
//
//nolint:gocyclo
func (g *Grub) Install(fallback string, config interface{}, sequence runtime.Sequence) (err error) {
grubcfg, ok := config.(*Cfg)
if !ok {
return errors.New("expected a grub config")
// Put puts a new menu entry to the grub config (nothing is written to disk).
func (c *Config) Put(entry BootLabel, cmdline string) error {
c.Entries[entry] = buildMenuEntry(entry, cmdline)
return nil
}
func (c *Config) validate() error {
if _, ok := c.Entries[c.Default]; !ok {
return fmt.Errorf("invalid default entry: %s", c.Default)
}
if err = writeCfg(GrubConfig, grubcfg); err != nil {
return err
}
dev, err := blockdevice.Open(g.BootDisk)
if err != nil {
return err
}
//nolint:errcheck
defer dev.Close()
// verify that BootDisk has boot partition
_, err = dev.GetPartition(constants.BootPartitionLabel)
if err != nil {
return err
}
blk := dev.Device().Name()
loopDevice := strings.HasPrefix(blk, "/dev/loop")
var platforms []string
switch g.Arch {
case amd64:
platforms = []string{"x86_64-efi", "i386-pc"}
case arm64:
platforms = []string{"arm64-efi"}
}
if goruntime.GOARCH == amd64 && g.Arch == amd64 && !loopDevice {
// let grub choose the platform automatically if not building an image
platforms = []string{""}
}
for _, platform := range platforms {
args := []string{"--boot-directory=" + constants.BootMountPoint, "--efi-directory=" + constants.EFIMountPoint, "--removable"}
if loopDevice {
args = append(args, "--no-nvram")
if c.Fallback != "" {
if _, ok := c.Entries[c.Fallback]; !ok {
return fmt.Errorf("invalid fallback entry: %s", c.Fallback)
}
}
if platform != "" {
args = append(args, "--target="+platform)
}
args = append(args, blk)
log.Printf("executing: grub-install %s", strings.Join(args, " "))
cmd := exec.Command("grub-install", args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err = cmd.Run(); err != nil {
return fmt.Errorf("failed to install grub: %w", err)
}
if c.Default == c.Fallback {
return fmt.Errorf("default and fallback entries must not be the same")
}
return nil
}
// Default implements the bootloader interface.
func (g *Grub) Default(label string) (err error) {
var b []byte
if b, err = ioutil.ReadFile(GrubConfig); err != nil {
return err
func buildMenuEntry(entry BootLabel, cmdline string) MenuEntry {
return MenuEntry{
Name: fmt.Sprintf("%s - %s", entry, version.Short()),
Linux: filepath.Join("/", string(entry), constants.KernelAsset),
Cmdline: cmdline,
Initrd: filepath.Join("/", string(entry), constants.InitramfsAsset),
}
re := regexp.MustCompile(`^set default="(.*)"`)
b = re.ReplaceAll(b, []byte(fmt.Sprintf(`set default="%s"`, label)))
log.Printf("writing %s to disk", GrubConfig)
return ioutil.WriteFile(GrubConfig, b, 0o600)
}
func writeCfg(path string, grubcfg *Cfg) (err error) {
b := []byte{}
wr := bytes.NewBuffer(b)
t := template.Must(template.New("grub").Parse(grubCfgTpl))
if err = t.Execute(wr, grubcfg); err != nil {
return err
}
dir := filepath.Dir(path)
if err = os.MkdirAll(dir, os.ModeDir); err != nil {
return err
}
log.Printf("writing %s to disk", path)
return ioutil.WriteFile(path, wr.Bytes(), 0o600)
}

View File

@ -0,0 +1,224 @@
// 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_test
import (
"bufio"
"bytes"
_ "embed"
"fmt"
"io"
"io/ioutil"
"os"
"regexp"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/talos-systems/talos/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub"
"github.com/talos-systems/talos/pkg/version"
)
var (
//go:embed testdata/grub_parse_test.cfg
grubCfg []byte
//go:embed testdata/grub_write_test.cfg
newConfig string
)
func TestDecode(t *testing.T) {
conf, err := grub.Decode(grubCfg)
assert.NoError(t, err)
assert.Equal(t, grub.BootA, conf.Default)
assert.Equal(t, grub.BootB, conf.Fallback)
assert.Len(t, conf.Entries, 2)
a := conf.Entries[grub.BootA]
assert.Equal(t, "A - v1", a.Name)
assert.True(t, strings.HasPrefix(a.Linux, "/A/"))
assert.True(t, strings.HasPrefix(a.Initrd, "/A/"))
assert.Equal(t, "cmdline A", a.Cmdline)
b := conf.Entries[grub.BootB]
assert.Equal(t, "B - v2", b.Name)
assert.Equal(t, "cmdline B", b.Cmdline)
assert.True(t, strings.HasPrefix(b.Linux, "/B/"))
assert.True(t, strings.HasPrefix(b.Initrd, "/B/"))
}
func TestParseBootLabel(t *testing.T) {
label, err := grub.ParseBootLabel("A - v1")
assert.NoError(t, err)
assert.Equal(t, grub.BootA, label)
label, err = grub.ParseBootLabel("B - v2")
assert.NoError(t, err)
assert.Equal(t, grub.BootB, label)
_, err = grub.ParseBootLabel("C - v3")
assert.Error(t, err)
}
//nolint:errcheck
func TestWrite(t *testing.T) {
oldName, oldTag := version.Name, version.Tag
t.Cleanup(func() {
version.Name, version.Tag = oldName, oldTag
})
version.Name = "Test"
version.Tag = "v0.0.1"
tempFile, _ := ioutil.TempFile("", "talos-test-grub-*.cfg")
defer os.Remove(tempFile.Name())
config := grub.NewConfig("cmdline A")
err := config.Write(tempFile.Name())
assert.NoError(t, err)
written, _ := ioutil.ReadFile(tempFile.Name())
assert.Equal(t, newConfig, string(written))
}
func TestPut(t *testing.T) {
config := grub.NewConfig("cmdline A")
err := config.Put(grub.BootB, "cmdline B")
assert.NoError(t, err)
assert.Len(t, config.Entries, 2)
assert.Equal(t, "cmdline B", config.Entries[grub.BootB].Cmdline)
err = config.Put(grub.BootA, "cmdline A 2")
assert.NoError(t, err)
assert.Equal(t, "cmdline A 2", config.Entries[grub.BootA].Cmdline)
}
//nolint:errcheck
func TestFallback(t *testing.T) {
config := grub.NewConfig("cmdline A")
_ = config.Put(grub.BootB, "cmdline B")
config.Fallback = grub.BootB
var buf bytes.Buffer
_ = config.Encode(&buf)
result := buf.String()
assert.Contains(t, result, `set fallback="B - `)
buf.Reset()
config.Fallback = ""
_ = config.Encode(&buf)
result = buf.String()
assert.NotContains(t, result, "set fallback")
}
type bootEntry struct {
Linux string
Initrd string
Cmdline string
}
// oldParser is the kexec parser used before the GRUB parser was rewritten.
//
// This makes sure Talos 0.14 can kexec into newly written GRUB config.
//
//nolint:gocyclo
func oldParser(r io.Reader) (*bootEntry, error) {
scanner := bufio.NewScanner(r)
entry := &bootEntry{}
var (
defaultEntry string
currentEntry string
)
for scanner.Scan() {
line := scanner.Text()
switch {
case strings.HasPrefix(line, "set default"):
matches := regexp.MustCompile(`set default="(.*)"`).FindStringSubmatch(line)
if len(matches) != 2 {
return nil, fmt.Errorf("malformed default entry: %q", line)
}
defaultEntry = matches[1]
case strings.HasPrefix(line, "menuentry"):
matches := regexp.MustCompile(`menuentry "(.*)"`).FindStringSubmatch(line)
if len(matches) != 2 {
return nil, fmt.Errorf("malformed menuentry: %q", line)
}
currentEntry = matches[1]
case strings.HasPrefix(line, " linux "):
if currentEntry != defaultEntry {
continue
}
parts := strings.SplitN(line[8:], " ", 2)
entry.Linux = parts[0]
if len(parts) == 2 {
entry.Cmdline = parts[1]
}
case strings.HasPrefix(line, " initrd "):
if currentEntry != defaultEntry {
continue
}
entry.Initrd = line[9:]
}
}
if entry.Linux == "" || entry.Initrd == "" {
return nil, scanner.Err()
}
return entry, scanner.Err()
}
func TestBackwardsCompat(t *testing.T) {
oldName, oldTag := version.Name, version.Tag
t.Cleanup(func() {
version.Name, version.Tag = oldName, oldTag
})
version.Name = "Test"
version.Tag = "v0.0.1"
var buf bytes.Buffer
config := grub.NewConfig("cmdline A")
require.NoError(t, config.Put(grub.BootB, "cmdline B"))
config.Default = grub.BootB
err := config.Encode(&buf)
assert.NoError(t, err)
entry, err := oldParser(&buf)
require.NoError(t, err)
assert.Equal(t, &bootEntry{
Linux: "/B/vmlinuz",
Initrd: "/B/initramfs.xz",
Cmdline: "cmdline B",
}, entry)
}

View File

@ -0,0 +1,99 @@
// 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"
"log"
"os"
"os/exec"
"runtime"
"strings"
"github.com/talos-systems/go-blockdevice/blockdevice"
"github.com/talos-systems/talos/pkg/machinery/constants"
)
const (
amd64 = "amd64"
arm64 = "arm64"
)
// Install validates the grub configuration and writes it to the disk.
//nolint:gocyclo
func (c *Config) Install(bootDisk, arch string) error {
if err := c.Write(ConfigPath); err != nil {
return err
}
blk, err := getBlockDeviceName(bootDisk)
if err != nil {
return err
}
loopDevice := strings.HasPrefix(blk, "/dev/loop")
var platforms []string
switch arch {
case amd64:
platforms = []string{"x86_64-efi", "i386-pc"}
case arm64:
platforms = []string{"arm64-efi"}
}
if runtime.GOARCH == amd64 && arch == amd64 && !loopDevice {
// let grub choose the platform automatically if not building an image
platforms = []string{""}
}
for _, platform := range platforms {
args := []string{"--boot-directory=" + constants.BootMountPoint, "--efi-directory=" +
constants.EFIMountPoint, "--removable"}
if loopDevice {
args = append(args, "--no-nvram")
}
if platform != "" {
args = append(args, "--target="+platform)
}
args = append(args, blk)
log.Printf("executing: grub-install %s", strings.Join(args, " "))
cmd := exec.Command("grub-install", args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err = cmd.Run(); err != nil {
return fmt.Errorf("failed to install grub: %w", err)
}
}
return nil
}
func getBlockDeviceName(bootDisk string) (string, error) {
dev, err := blockdevice.Open(bootDisk)
if err != nil {
return "", err
}
//nolint:errcheck
defer dev.Close()
// verify that BootDisk has boot partition
_, err = dev.GetPartition(constants.BootPartitionLabel)
if err != nil {
return "", err
}
blk := dev.Device().Name()
return blk, nil
}

View File

@ -0,0 +1,22 @@
set default="A - v1"
set timeout=3
set fallback="B - v2"
insmod all_video
terminal_input console
terminal_output console
menuentry "A - v1" {
set gfxmode=auto
set gfxpayload=text
linux /A/vmlinuz cmdline A
initrd /A/initramfs.xz
}
menuentry "B - v2" {
set gfxmode=auto
set gfxpayload=text
linux /B/vmlinuz cmdline B
initrd /B/initramfs.xz
}

View File

@ -0,0 +1,15 @@
set default="A - Test v0.0.1"
set timeout=3
insmod all_video
terminal_input console
terminal_output console
menuentry "A - Test v0.0.1" {
set gfxmode=auto
set gfxpayload=text
linux /A/vmlinuz cmdline A
initrd /A/initramfs.xz
}

View File

@ -112,9 +112,18 @@ func (m *Meta) Revert() (err error) {
return m.Write()
}
g := &grub.Grub{}
conf, err := grub.Read(grub.ConfigPath)
if err != nil {
return err
}
if err = g.Default(label); err != nil {
bootEntry, err := grub.ParseBootLabel(label)
if err != nil {
return err
}
conf.Default = bootEntry
if err = conf.Write(grub.ConfigPath); err != nil {
return err
}

View File

@ -1,25 +0,0 @@
// 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 syslinux
import "github.com/talos-systems/talos/pkg/machinery/constants"
const (
// BootA is a syslinux label.
BootA = "boot-a"
// BootB is a syslinux label.
BootB = "boot-b"
// SyslinuxLdlinux is the path to ldlinux.sys.
SyslinuxLdlinux = constants.BootMountPoint + "/syslinux/ldlinux.sys"
// SyslinuxConfig is the path to the Syslinux config.
SyslinuxConfig = constants.BootMountPoint + "/syslinux/syslinux.cfg"
gptmbrbin = "/usr/lib/syslinux/gptmbr.bin"
syslinuxefi = "/usr/lib/syslinux/syslinux.efi"
ldlinuxe64 = "/usr/lib/syslinux/ldlinux.e64"
)

View File

@ -1,314 +0,0 @@
// 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 syslinux
import (
"bytes"
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"path/filepath"
"regexp"
"text/template"
"github.com/talos-systems/go-cmd/pkg/cmd"
"golang.org/x/sys/unix"
"github.com/talos-systems/talos/internal/app/machined/pkg/runtime"
advcommon "github.com/talos-systems/talos/internal/app/machined/pkg/runtime/v1alpha1/bootloader/adv"
"github.com/talos-systems/talos/internal/app/machined/pkg/runtime/v1alpha1/bootloader/adv/syslinux"
"github.com/talos-systems/talos/pkg/machinery/constants"
)
const syslinuxCfgTpl = `DEFAULT {{ .Default }}
PROMPT 1
TIMEOUT 50
{{- range .Labels }}
INCLUDE /{{ .Root }}/include.cfg
{{- end }}`
const syslinuxLabelTpl = `LABEL {{ .Root }}
KERNEL {{ .Kernel }}
INITRD {{ .Initrd }}
APPEND {{ .Append }}
`
// Cfg reprsents the cfg file.
type Cfg struct {
Default string
Labels []*Label
}
// Label reprsents a label in the cfg file.
type Label struct {
Root string
Kernel string
Initrd string
Append string
}
// Syslinux represents the syslinux bootloader.
type Syslinux struct{}
// Prepare implements the Bootloader interface. It works by writing
// gptmbr.bin to a block device.
func Prepare(dev string) (err error) {
b, err := ioutil.ReadFile(gptmbrbin)
if err != nil {
return err
}
f, err := os.OpenFile(dev, os.O_WRONLY|unix.O_CLOEXEC, os.ModeDevice)
if err != nil {
return err
}
//nolint:errcheck
defer f.Close()
if _, err := f.Write(b); err != nil {
return err
}
return nil
}
// Install implements the Bootloader interface. It sets up syslinux with the
// specified kernel parameters.
func Install(fallback string, config interface{}, sequence runtime.Sequence, bootPartitionFound bool) (err error) {
syslinuxcfg, ok := config.(*Cfg)
if !ok {
return errors.New("expected a syslinux config")
}
if err = writeCfg(constants.BootMountPoint, SyslinuxConfig, syslinuxcfg); err != nil {
return err
}
if sequence == runtime.SequenceUpgrade && bootPartitionFound {
log.Println("updating syslinux")
if err = update(); err != nil {
return err
}
} else {
log.Println("installing syslinux")
if err = install(); err != nil {
return err
}
}
if err = writeUEFIFiles(); err != nil {
return err
}
if sequence == runtime.SequenceUpgrade {
if err = setADV(SyslinuxLdlinux, fallback); err != nil {
return err
}
}
return nil
}
// Labels parses the syslinux config and returns the current active label, and
// what should be the next label.
func Labels() (current, next string, err error) {
var b []byte
if b, err = ioutil.ReadFile(SyslinuxConfig); err != nil {
if errors.Is(err, os.ErrNotExist) {
current = BootA
return current, "", nil
}
return "", "", err
}
re := regexp.MustCompile(`^DEFAULT\s(.*)`)
matches := re.FindSubmatch(b)
if len(matches) != 2 {
return "", "", fmt.Errorf("expected 2 matches, got %d", len(matches))
}
current = string(matches[1])
switch current {
case BootA:
next = BootB
case BootB:
next = BootA
default:
return "", "", fmt.Errorf("unknown syslinux label: %q", current)
}
return current, next, err
}
// Default sets the default syslinx label.
func Default(label string) (err error) {
log.Printf("setting default label to %q", label)
var b []byte
if b, err = ioutil.ReadFile(SyslinuxConfig); err != nil {
return err
}
re := regexp.MustCompile(`^DEFAULT\s(.*)`)
matches := re.FindSubmatch(b)
if len(matches) != 2 {
return fmt.Errorf("expected 2 matches, got %d", len(matches))
}
b = re.ReplaceAll(b, []byte(fmt.Sprintf("DEFAULT %s", label)))
return ioutil.WriteFile(SyslinuxConfig, b, 0o600)
}
func writeCfg(base, path string, syslinuxcfg *Cfg) (err error) {
b := []byte{}
wr := bytes.NewBuffer(b)
t := template.Must(template.New("syslinux").Parse(syslinuxCfgTpl))
if err = t.Execute(wr, syslinuxcfg); err != nil {
return err
}
dir := filepath.Dir(path)
if err = os.MkdirAll(dir, os.ModeDir); err != nil {
return err
}
log.Printf("writing %s to disk", path)
if err = ioutil.WriteFile(path, wr.Bytes(), 0o600); err != nil {
return err
}
for _, label := range syslinuxcfg.Labels {
b = []byte{}
wr = bytes.NewBuffer(b)
t = template.Must(template.New("syslinux").Parse(syslinuxLabelTpl))
if err = t.Execute(wr, label); err != nil {
return err
}
dir = filepath.Join(base, label.Root)
if err = os.MkdirAll(dir, os.ModeDir); err != nil {
return err
}
log.Printf("writing syslinux label %q to disk", label.Root)
if err = ioutil.WriteFile(filepath.Join(dir, "include.cfg"), wr.Bytes(), 0o600); err != nil {
return err
}
}
return nil
}
func copyFile(src, dst string) error {
info, err := os.Stat(src)
if err != nil {
return err
}
if !info.Mode().IsRegular() {
return fmt.Errorf("%q is not a regular file", src)
}
s, err := os.Open(src)
if err != nil {
return err
}
//nolint:errcheck
defer s.Close()
d, err := os.Create(dst)
if err != nil {
return err
}
//nolint:errcheck
defer d.Close()
_, err = io.Copy(d, s)
return err
}
func writeUEFIFiles() (err error) {
dir := filepath.Join(constants.BootMountPoint, "EFI", "BOOT")
if err = os.MkdirAll(dir, 0o700); err != nil {
return err
}
for src, dest := range map[string]string{syslinuxefi: filepath.Join(dir, "BOOTX64.EFI"), ldlinuxe64: filepath.Join(dir, "ldlinux.e64")} {
if err = copyFile(src, dest); err != nil {
return err
}
}
return nil
}
func install() (err error) {
_, err = cmd.Run("extlinux", "--install", filepath.Dir(SyslinuxConfig))
if err != nil {
return fmt.Errorf("failed to install syslinux: %w", err)
}
return nil
}
func update() (err error) {
if _, err = cmd.Run("extlinux", "--update", filepath.Dir(SyslinuxConfig)); err != nil {
return fmt.Errorf("failed to update syslinux: %w", err)
}
return nil
}
func setADV(ldlinux, fallback string) (err error) {
var f *os.File
if f, err = os.OpenFile(ldlinux, os.O_RDWR, 0o700); err != nil {
return err
}
//nolint:errcheck
defer f.Close()
var adv syslinux.ADV
if adv, err = syslinux.NewADV(f); err != nil {
return err
}
if ok := adv.SetTag(advcommon.Upgrade, fallback); !ok {
return fmt.Errorf("failed to set upgrade tag: %q", fallback)
}
if _, err = f.Write(adv); err != nil {
return err
}
log.Printf("set fallback to %q", fallback)
return nil
}

View File

@ -200,10 +200,6 @@ func (*Sequencer) Boot(r runtime.Runtime) []runtime.Phase {
r.State().Platform().Mode() != runtime.ModeContainer,
"ephemeral",
MountEphemeralPartition,
).AppendWhen(
r.State().Platform().Mode() != runtime.ModeContainer,
"verifyInstall",
VerifyInstallation,
).Append(
"var",
SetupVarDirectory,

View File

@ -682,37 +682,6 @@ func StopAllServices(seq runtime.Sequence, data interface{}) (runtime.TaskExecut
}, "stopAllServices"
}
// VerifyInstallation represents the VerifyInstallation task.
func VerifyInstallation(seq runtime.Sequence, data interface{}) (runtime.TaskExecutionFunc, string) {
return func(ctx context.Context, logger *log.Logger, r runtime.Runtime) (err error) {
var (
current string
next string
disk string
)
disk, err = r.Config().Machine().Install().Disk()
if err != nil {
return err
}
grub := &grub.Grub{
BootDisk: disk,
}
current, next, err = grub.Labels()
if err != nil {
return err
}
if current == "" && next == "" {
return fmt.Errorf("bootloader is not configured")
}
return err
}, "verifyInstallation"
}
// MountOverlayFilesystems represents the MountOverlayFilesystems task.
func MountOverlayFilesystems(seq runtime.Sequence, data interface{}) (runtime.TaskExecutionFunc, string) {
return func(ctx context.Context, logger *log.Logger, r runtime.Runtime) (err error) {
@ -1740,26 +1709,22 @@ func KexecPrepare(seq runtime.Sequence, data interface{}) (runtime.TaskExecution
return nil
}
disk, err := r.Config().Machine().Install().Disk()
conf, err := grub.Read(grub.ConfigPath)
if err != nil {
return err
}
grub := &grub.Grub{
BootDisk: disk,
}
entry, err := grub.GetCurrentEntry()
if err != nil {
return err
}
if entry == nil {
if conf == nil {
return nil
}
kernelPath := filepath.Join(constants.BootMountPoint, entry.Linux)
initrdPath := filepath.Join(constants.BootMountPoint, entry.Initrd)
defaultEntry, ok := conf.Entries[conf.Default]
if !ok {
return nil
}
kernelPath := filepath.Join(constants.BootMountPoint, defaultEntry.Linux)
initrdPath := filepath.Join(constants.BootMountPoint, defaultEntry.Initrd)
kernel, err := os.Open(kernelPath)
if err != nil {
@ -1775,7 +1740,7 @@ func KexecPrepare(seq runtime.Sequence, data interface{}) (runtime.TaskExecution
defer initrd.Close() //nolint:errcheck
cmdline := strings.TrimSpace(entry.Cmdline)
cmdline := strings.TrimSpace(defaultEntry.Cmdline)
if err = unix.KexecFileLoad(int(kernel.Fd()), int(initrd.Fd()), cmdline, 0); err != nil {
switch {

View File

@ -77,5 +77,10 @@ func printLong(w io.Writer, v *machineapi.VersionInfo) {
// PrintShortVersion prints the tag and SHA.
func PrintShortVersion() {
fmt.Printf("%s %s-%s\n", Name, Tag, SHA)
fmt.Println(Short())
}
// Short returns the short version string consist of name, tag and SHA.
func Short() string {
return fmt.Sprintf("%s %s", Name, Tag)
}