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:
parent
6ccfdbaf1b
commit
4d5cd66538
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
@ -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"
|
||||
)
|
||||
|
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
@ -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
|
||||
}
|
22
internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/testdata/grub_parse_test.cfg
vendored
Normal file
22
internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/testdata/grub_parse_test.cfg
vendored
Normal 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
|
||||
}
|
15
internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/testdata/grub_write_test.cfg
vendored
Normal file
15
internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/testdata/grub_write_test.cfg
vendored
Normal 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
|
||||
}
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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"
|
||||
)
|
@ -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
|
||||
}
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user