From 0a30ef78456e854419d0c593f9c97f40166102f3 Mon Sep 17 00:00:00 2001 From: Andrey Smirnov Date: Tue, 19 Dec 2023 21:23:37 +0400 Subject: [PATCH] fix: imager should support different Talos versions Add some quirks to make images generated with newer Talos compatible with images generated by older Talos. Specifically, reset options were adding in Talos 1.4, so we shouldn't add them for older versions. Signed-off-by: Andrey Smirnov --- cmd/installer/pkg/install/install.go | 2 +- .../runtime/v1alpha1/bootloader/bootloader.go | 8 +++- .../v1alpha1/bootloader/grub/decode.go | 22 ++++++----- .../v1alpha1/bootloader/grub/encode.go | 2 + .../runtime/v1alpha1/bootloader/grub/grub.go | 12 +++--- .../v1alpha1/bootloader/grub/grub_test.go | 30 +++++++++++++++ .../testdata/grub_write_no_reset_test.cfg | 15 ++++++++ pkg/imager/iso/grub.cfg | 2 + pkg/imager/iso/grub.go | 8 +++- pkg/imager/out.go | 1 + pkg/imager/quirks/quirks.go | 35 ++++++++++++++++++ pkg/imager/quirks/quirks_test.go | 37 +++++++++++++++++++ 12 files changed, 155 insertions(+), 19 deletions(-) create mode 100644 internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/testdata/grub_write_no_reset_test.cfg create mode 100644 pkg/imager/quirks/quirks.go create mode 100644 pkg/imager/quirks/quirks_test.go diff --git a/cmd/installer/pkg/install/install.go b/cmd/installer/pkg/install/install.go index 1615ddfa71..aa5dc731a9 100644 --- a/cmd/installer/pkg/install/install.go +++ b/cmd/installer/pkg/install/install.go @@ -157,7 +157,7 @@ func NewInstaller(ctx context.Context, cmdline *procfs.Cmdline, mode Mode, opts if !bootLoaderPresent { if mode.IsImage() { // on image creation, use the bootloader based on options - i.bootloader = bootloader.New(opts.ImageSecureboot) + i.bootloader = bootloader.New(opts.ImageSecureboot, opts.Version) } else { // on install/upgrade perform automatic detection i.bootloader = bootloader.NewAuto() diff --git a/internal/app/machined/pkg/runtime/v1alpha1/bootloader/bootloader.go b/internal/app/machined/pkg/runtime/v1alpha1/bootloader/bootloader.go index 4dc833855f..a4d8a556f0 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/bootloader/bootloader.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/bootloader/bootloader.go @@ -12,6 +12,7 @@ import ( "github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub" "github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1/bootloader/options" "github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1/bootloader/sdboot" + "github.com/siderolabs/talos/pkg/imager/quirks" ) // Bootloader describes a bootloader. @@ -62,10 +63,13 @@ func NewAuto() Bootloader { } // New returns a new bootloader based on the secureboot flag. -func New(secureboot bool) Bootloader { +func New(secureboot bool, talosVersion string) Bootloader { if secureboot { return sdboot.New() } - return grub.NewConfig() + g := grub.NewConfig() + g.AddResetOption = quirks.New(talosVersion).SupportsResetGRUBOption() + + return g } diff --git a/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/decode.go b/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/decode.go index 740e201896..94bdd7a2d4 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/decode.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/decode.go @@ -69,44 +69,48 @@ func Decode(c []byte) (*Config, error) { return nil, err } - entries, err := parseEntries(c) + entries, hasResetOption, err := parseEntries(c) if err != nil { return nil, err } conf := Config{ - Default: defaultEntry, - Fallback: fallbackEntry, - Entries: entries, + Default: defaultEntry, + Fallback: fallbackEntry, + Entries: entries, + AddResetOption: hasResetOption, } return &conf, nil } -func parseEntries(conf []byte) (map[BootLabel]MenuEntry, error) { +func parseEntries(conf []byte) (map[BootLabel]MenuEntry, bool, error) { entries := make(map[BootLabel]MenuEntry) + hasResetOption := false matches := menuEntryRegex.FindAllSubmatch(conf, -1) for _, m := range matches { if len(m) != 3 { - return nil, fmt.Errorf("conf block: expected 3 matches, got %d", len(m)) + return nil, false, fmt.Errorf("conf block: expected 3 matches, got %d", len(m)) } confBlock := m[2] linux, cmdline, initrd, err := parseConfBlock(confBlock) if err != nil { - return nil, err + return nil, false, err } name := string(m[1]) bootEntry, err := ParseBootLabel(name) if err != nil { - return nil, err + return nil, false, err } if bootEntry == BootReset { + hasResetOption = true + continue } @@ -118,7 +122,7 @@ func parseEntries(conf []byte) (map[BootLabel]MenuEntry, error) { } } - return entries, nil + return entries, hasResetOption, nil } func parseConfBlock(block []byte) (linux, cmdline, initrd string, err error) { diff --git a/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/encode.go b/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/encode.go index 73af2a4423..cbf5b63715 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/encode.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/encode.go @@ -32,6 +32,7 @@ menuentry "{{ $entry.Name }}" { } {{ end -}} +{{ if .AddResetOption -}} {{ $defaultEntry := index .Entries .Default -}} menuentry "Reset Talos installation and return to maintenance mode" { set gfxmode=auto @@ -39,6 +40,7 @@ menuentry "Reset Talos installation and return to maintenance mode" { linux {{ $defaultEntry.Linux }} {{ quote $defaultEntry.Cmdline }} talos.experimental.wipe=system:EPHEMERAL,STATE initrd {{ $defaultEntry.Initrd }} } +{{ end -}} ` // Write the grub configuration to the given file. diff --git a/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/grub.go b/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/grub.go index 38afb68bd5..7891222994 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/grub.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/grub.go @@ -15,9 +15,10 @@ import ( // Config represents a grub configuration file (grub.cfg). type Config struct { - Default BootLabel - Fallback BootLabel - Entries map[BootLabel]MenuEntry + Default BootLabel + Fallback BootLabel + Entries map[BootLabel]MenuEntry + AddResetOption bool } // MenuEntry represents a grub menu entry in the grub config file. @@ -35,8 +36,9 @@ func (e bootloaderNotInstalledError) Error() string { // NewConfig creates a new grub configuration (nothing is written to disk). func NewConfig() *Config { return &Config{ - Default: BootA, - Entries: map[BootLabel]MenuEntry{}, + Default: BootA, + Entries: map[BootLabel]MenuEntry{}, + AddResetOption: true, } } diff --git a/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/grub_test.go b/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/grub_test.go index 8c7dd1caaa..a144977471 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/grub_test.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/grub_test.go @@ -28,6 +28,9 @@ var ( //go:embed testdata/grub_write_test.cfg newConfig string + + //go:embed testdata/grub_write_no_reset_test.cfg + newNoResetConfig string ) func TestDecode(t *testing.T) { @@ -50,6 +53,8 @@ func TestDecode(t *testing.T) { assert.Equal(t, "cmdline B", b.Cmdline) assert.True(t, strings.HasPrefix(b.Linux, "/B/")) assert.True(t, strings.HasPrefix(b.Initrd, "/B/")) + + assert.True(t, conf.AddResetOption) } func TestEncodeDecode(t *testing.T) { @@ -110,6 +115,31 @@ func TestWrite(t *testing.T) { assert.Equal(t, newConfig, string(written)) } +//nolint:errcheck +func TestWriteNoReset(t *testing.T) { + oldName := version.Name + + t.Cleanup(func() { + version.Name = oldName + }) + + version.Name = "TestOld" + + tempFile, _ := os.CreateTemp("", "talos-test-grub-*.cfg") + + t.Cleanup(func() { require.NoError(t, os.Remove(tempFile.Name())) }) + + config := grub.NewConfig() + config.AddResetOption = false + require.NoError(t, config.Put(grub.BootA, "cmdline A", "v0.0.1")) + + err := config.Write(tempFile.Name(), t.Logf) + assert.NoError(t, err) + + written, _ := os.ReadFile(tempFile.Name()) + assert.Equal(t, newNoResetConfig, string(written)) +} + func TestPut(t *testing.T) { config := grub.NewConfig() require.NoError(t, config.Put(grub.BootA, "cmdline A", "v1.2.3")) diff --git a/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/testdata/grub_write_no_reset_test.cfg b/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/testdata/grub_write_no_reset_test.cfg new file mode 100644 index 0000000000..44632d3513 --- /dev/null +++ b/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/testdata/grub_write_no_reset_test.cfg @@ -0,0 +1,15 @@ +set default="A - TestOld v0.0.1" + +set timeout=3 + +insmod all_video + +terminal_input console +terminal_output console + +menuentry "A - TestOld v0.0.1" { + set gfxmode=auto + set gfxpayload=text + linux /A/vmlinuz cmdline A + initrd /A/initramfs.xz +} diff --git a/pkg/imager/iso/grub.cfg b/pkg/imager/iso/grub.cfg index 158874bba3..ea61bff903 100644 --- a/pkg/imager/iso/grub.cfg +++ b/pkg/imager/iso/grub.cfg @@ -13,9 +13,11 @@ menuentry "Talos ISO" { initrd /boot/initramfs.xz } +{{ if .AddResetOption -}} menuentry "Reset Talos installation" { set gfxmode=auto set gfxpayload=text linux /boot/vmlinuz {{ quote .Cmdline }} talos.experimental.wipe=system initrd /boot/initramfs.xz } +{{ end -}} diff --git a/pkg/imager/iso/grub.go b/pkg/imager/iso/grub.go index 3818811a15..1feec3a406 100644 --- a/pkg/imager/iso/grub.go +++ b/pkg/imager/iso/grub.go @@ -16,6 +16,7 @@ import ( "github.com/siderolabs/go-cmd/pkg/cmd" "github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub" + "github.com/siderolabs/talos/pkg/imager/quirks" "github.com/siderolabs/talos/pkg/imager/utils" ) @@ -24,6 +25,7 @@ type GRUBOptions struct { KernelPath string InitramfsPath string Cmdline string + Version string ScratchDir string @@ -59,9 +61,11 @@ func CreateGRUB(printf func(string, ...any), options GRUBOptions) error { } if err = tmpl.Execute(&grubCfg, struct { - Cmdline string + Cmdline string + AddResetOption bool }{ - Cmdline: options.Cmdline, + Cmdline: options.Cmdline, + AddResetOption: quirks.New(options.Version).SupportsResetGRUBOption(), }); err != nil { return err } diff --git a/pkg/imager/out.go b/pkg/imager/out.go index 90ab0f3374..153f0dcc8a 100644 --- a/pkg/imager/out.go +++ b/pkg/imager/out.go @@ -153,6 +153,7 @@ func (i *Imager) outISO(ctx context.Context, path string, report *reporter.Repor KernelPath: i.prof.Input.Kernel.Path, InitramfsPath: i.initramfsPath, Cmdline: i.cmdline, + Version: i.prof.Version, ScratchDir: scratchSpace, OutPath: path, diff --git a/pkg/imager/quirks/quirks.go b/pkg/imager/quirks/quirks.go new file mode 100644 index 0000000000..63768e0008 --- /dev/null +++ b/pkg/imager/quirks/quirks.go @@ -0,0 +1,35 @@ +// 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 quirks contains the quirks for Talos image generation. +package quirks + +import "github.com/blang/semver/v4" + +// Quirks contains the quirks for Talos image generation. +type Quirks struct { + v *semver.Version +} + +// New returns a new Quirks instance based on Talos version for the image. +func New(talosVersion string) Quirks { + v, err := semver.ParseTolerant(talosVersion) // ignore the error + if err != nil { + return Quirks{} + } + + return Quirks{v: &v} +} + +var minVersionResetOption = semver.MustParse("1.4.0") + +// SupportsResetGRUBOption returns true if the Talos version supports the reset option in GRUB menu (image and ISO). +func (q Quirks) SupportsResetGRUBOption() bool { + // if the version doesn't parse, we assume it's latest Talos + if q.v == nil { + return true + } + + return q.v.GTE(minVersionResetOption) +} diff --git a/pkg/imager/quirks/quirks_test.go b/pkg/imager/quirks/quirks_test.go new file mode 100644 index 0000000000..852325e65c --- /dev/null +++ b/pkg/imager/quirks/quirks_test.go @@ -0,0 +1,37 @@ +// 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 quirks_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/siderolabs/talos/pkg/imager/quirks" +) + +func TestSupportsResetOption(t *testing.T) { + for _, test := range []struct { + version string + + expected bool + }{ + { + version: "1.5.0", + expected: true, + }, + { + expected: true, + }, + { + version: "1.3.7", + expected: false, + }, + } { + t.Run(test.version, func(t *testing.T) { + assert.Equal(t, test.expected, quirks.New(test.version).SupportsResetGRUBOption()) + }) + } +}