From 56b27239192b0194f3849cb8d117c52587f09e49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20L=C3=B6nnegren?= Date: Mon, 22 Jan 2024 18:46:36 +0100 Subject: [PATCH] Install grub.cfg into EFI System Partition (#1904) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Install grub.cfg into EFI System Partition Change the bootloader install logic to install the grub.cfg into the EFI System Partition (ESP). This needs some changes to how root is set in the grub.cfg as well as moving grub_oem_env file to ESP when created in install/reset/upgrade command. Install grub-modules to both EFI/BOOT and EFI/ELEMENTAL. Signed-off-by: Fredrik Lönnegren --- .github/workflows/build_and_test_x86.yaml | 4 + pkg/action/build-disk.go | 8 +- pkg/action/build-iso.go | 4 +- pkg/action/install.go | 8 +- pkg/action/install_test.go | 4 +- pkg/action/reset.go | 8 +- pkg/action/upgrade.go | 9 +- pkg/action/upgrade_test.go | 6 +- pkg/bootloader/bootloader_suite_test.go | 2 +- pkg/bootloader/grub.go | 91 ++++++++++--------- pkg/bootloader/grub_test.go | 46 ++++++---- pkg/config/config.go | 3 + pkg/config/config_test.go | 6 -- pkg/elemental/elemental.go | 4 +- pkg/elemental/elemental_test.go | 4 +- .../system/oem/03_branding.yaml | 21 ++++- .../system/oem/08_boot_assessment.yaml | 56 ++++++------ .../grub-config/etc/elemental/grub.cfg | 2 + pkg/types/v1/bootloader.go | 2 +- pkg/types/v1/config_test.go | 2 +- tests/fallback/fallback_test.go | 8 +- tests/recovery/recovery_test.go | 8 +- tests/upgrade/upgrade_test.go | 4 +- tests/vm/sut.go | 13 ++- 24 files changed, 192 insertions(+), 131 deletions(-) diff --git a/.github/workflows/build_and_test_x86.yaml b/.github/workflows/build_and_test_x86.yaml index 4dd143ffade..2ce6adff64d 100644 --- a/.github/workflows/build_and_test_x86.yaml +++ b/.github/workflows/build_and_test_x86.yaml @@ -95,6 +95,10 @@ jobs: sudo rm -rf /usr/local/lib/android # will release about 10 GB if you don't need Android sudo rm -rf /usr/share/dotnet # will release about 20GB if you don't need .NET sudo df -h + - if: ${{ steps.cache-check.outputs.cache-hit != 'true' }} + name: Build toolkit + run: | + make build - if: ${{ steps.cache-check.outputs.cache-hit != 'true' }} name: Install to disk run: | diff --git a/pkg/action/build-disk.go b/pkg/action/build-disk.go index c8bd463459c..556184ef23e 100644 --- a/pkg/action/build-disk.go +++ b/pkg/action/build-disk.go @@ -178,7 +178,7 @@ func (b *BuildDiskAction) BuildDiskRun() (err error) { //nolint:gocyclo } // Install grub - err = b.bootloader.InstallConfig(activeRoot, b.roots[constants.StatePartName]) + err = b.bootloader.InstallConfig(activeRoot, b.roots[constants.EfiPartName]) if err != nil { b.cfg.Logger.Errorf("failed installing grub configuration: %s", err.Error()) return err @@ -199,7 +199,7 @@ func (b *BuildDiskAction) BuildDiskRun() (err error) { //nolint:gocyclo grubVars := b.spec.GetGrubLabels() err = b.bootloader.SetPersistentVariables( - filepath.Join(b.roots[constants.StatePartName], constants.GrubOEMEnv), + filepath.Join(b.roots[constants.EfiPartName], constants.GrubOEMEnv), grubVars, ) if err != nil { @@ -208,7 +208,7 @@ func (b *BuildDiskAction) BuildDiskRun() (err error) { //nolint:gocyclo } err = b.bootloader.InstallEFI( - activeRoot, b.roots[constants.StatePartName], + activeRoot, b.roots[constants.EfiPartName], b.roots[constants.EfiPartName], b.spec.Partitions.State.FilesystemLabel, ) if err != nil { @@ -217,7 +217,7 @@ func (b *BuildDiskAction) BuildDiskRun() (err error) { //nolint:gocyclo } // Rebrand - err = b.bootloader.SetDefaultEntry(b.roots[constants.StatePartName], activeRoot, b.spec.GrubDefEntry) + err = b.bootloader.SetDefaultEntry(b.roots[constants.EfiPartName], activeRoot, b.spec.GrubDefEntry) if err != nil { return elementalError.NewFromError(err, elementalError.SetDefaultGrubEntry) } diff --git a/pkg/action/build-iso.go b/pkg/action/build-iso.go index b53f7b29909..d4acec12eeb 100644 --- a/pkg/action/build-iso.go +++ b/pkg/action/build-iso.go @@ -30,7 +30,7 @@ import ( ) const ( - grubPrefixDir = "/boot/grub2" + grubPrefixDir = "/EFI/BOOT" isoBootCatalog = "/boot/boot.catalog" ) @@ -73,7 +73,7 @@ func NewBuildISOAction(cfg *v1.BuildConfig, spec *v1.LiveISO, opts ...BuildISOAc } if b.bootloader == nil { - b.bootloader = bootloader.NewGrub(&cfg.Config, bootloader.WithGrubPrefix(grubPrefixDir)) + b.bootloader = bootloader.NewGrub(&cfg.Config) } return b diff --git a/pkg/action/install.go b/pkg/action/install.go index 4c6438352c7..e0912f145ea 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -197,8 +197,8 @@ func (i InstallAction) Run() (err error) { // Install grub err = i.bootloader.Install( cnst.WorkingImgDir, - i.spec.Partitions.State.MountPoint, - i.spec.Partitions.State.FilesystemLabel, + i.spec.Partitions.EFI.MountPoint, + i.spec.Partitions.EFI.FilesystemLabel, ) if err != nil { return elementalError.NewFromError(err, elementalError.InstallGrub) @@ -221,7 +221,7 @@ func (i InstallAction) Run() (err error) { grubVars := i.spec.GetGrubLabels() err = i.bootloader.SetPersistentVariables( - filepath.Join(i.spec.Partitions.State.MountPoint, cnst.GrubOEMEnv), + filepath.Join(i.spec.Partitions.EFI.MountPoint, cnst.GrubOEMEnv), grubVars, ) if err != nil { @@ -231,7 +231,7 @@ func (i InstallAction) Run() (err error) { // Installation rebrand (only grub for now) err = i.bootloader.SetDefaultEntry( - i.spec.Partitions.State.MountPoint, + i.spec.Partitions.EFI.MountPoint, cnst.WorkingImgDir, i.spec.GrubDefEntry, ) diff --git a/pkg/action/install_test.go b/pkg/action/install_test.go index cedccae4306..1210f179015 100644 --- a/pkg/action/install_test.go +++ b/pkg/action/install_test.go @@ -169,7 +169,7 @@ var _ = Describe("Install action tests", func() { { Name: "device1", FilesystemLabel: "COS_GRUB", - Type: "ext4", + Type: "vfat", }, { Name: "device2", @@ -388,7 +388,7 @@ var _ = Describe("Install action tests", func() { spec.GrubDefEntry = "cOS" bootloader.ErrorSetDefaultEntry = true Expect(installer.Run()).NotTo(BeNil()) - Expect(runner.MatchMilestones([][]string{{"grub2-editenv", filepath.Join(constants.StateDir, constants.GrubOEMEnv)}})) + Expect(runner.MatchMilestones([][]string{{"grub2-editenv", filepath.Join(constants.EfiDir, constants.GrubOEMEnv)}})) }) }) }) diff --git a/pkg/action/reset.go b/pkg/action/reset.go index 8eb43ba899b..10fa90eaf7a 100644 --- a/pkg/action/reset.go +++ b/pkg/action/reset.go @@ -205,8 +205,8 @@ func (r ResetAction) Run() (err error) { // install grub err = r.bootloader.Install( cnst.WorkingImgDir, - r.spec.Partitions.State.MountPoint, - r.spec.Partitions.State.FilesystemLabel, + r.spec.Partitions.EFI.MountPoint, + r.spec.Partitions.EFI.FilesystemLabel, ) if err != nil { @@ -242,7 +242,7 @@ func (r ResetAction) Run() (err error) { grubVars := r.spec.GetGrubLabels() err = r.bootloader.SetPersistentVariables( - filepath.Join(r.spec.Partitions.State.MountPoint, cnst.GrubOEMEnv), + filepath.Join(r.spec.Partitions.EFI.MountPoint, cnst.GrubOEMEnv), grubVars, ) if err != nil { @@ -252,7 +252,7 @@ func (r ResetAction) Run() (err error) { // installation rebrand (only grub for now) err = r.bootloader.SetDefaultEntry( - r.spec.Partitions.State.MountPoint, + r.spec.Partitions.EFI.MountPoint, cnst.WorkingImgDir, r.spec.GrubDefEntry, ) diff --git a/pkg/action/upgrade.go b/pkg/action/upgrade.go index 5e6f028d21a..d59fea0bf9c 100644 --- a/pkg/action/upgrade.go +++ b/pkg/action/upgrade.go @@ -182,6 +182,11 @@ func (u *UpgradeAction) Run() (err error) { return elementalError.NewFromError(err, elementalError.MountRecoveryPartition) } cleanup.Push(umount) + umount, err = elemental.MountRWPartition(u.config.Config, u.spec.Partitions.EFI) + if err != nil { + return elementalError.NewFromError(err, elementalError.MountPartitions) + } + cleanup.Push(umount) // Cleanup transition image file before leaving cleanup.Push(func() error { return u.remove(upgradeImg.File) }) @@ -253,7 +258,7 @@ func (u *UpgradeAction) Run() (err error) { grubVars := u.spec.GetGrubLabels() err = u.bootloader.SetPersistentVariables( - filepath.Join(u.spec.Partitions.State.MountPoint, constants.GrubOEMEnv), + filepath.Join(u.spec.Partitions.EFI.MountPoint, constants.GrubOEMEnv), grubVars, ) if err != nil { @@ -265,7 +270,7 @@ func (u *UpgradeAction) Run() (err error) { if !u.spec.RecoveryUpgrade { u.Info("rebranding") - err = u.bootloader.SetDefaultEntry(u.spec.Partitions.State.MountPoint, constants.WorkingImgDir, u.spec.GrubDefEntry) + err = u.bootloader.SetDefaultEntry(u.spec.Partitions.EFI.MountPoint, constants.WorkingImgDir, u.spec.GrubDefEntry) if err != nil { u.Error("failed setting default entry") return elementalError.NewFromError(err, elementalError.SetDefaultGrubEntry) diff --git a/pkg/action/upgrade_test.go b/pkg/action/upgrade_test.go index 5d0bc1ca620..16ef9816a7e 100644 --- a/pkg/action/upgrade_test.go +++ b/pkg/action/upgrade_test.go @@ -98,6 +98,7 @@ var _ = Describe("Runtime Actions", func() { logger.SetLevel(logrus.DebugLevel) // Create paths used by tests + utils.MkdirAll(fs, constants.EfiDir, constants.DirPerm) utils.MkdirAll(fs, fmt.Sprintf("%s/cOS", constants.RunningStateDir), constants.DirPerm) utils.MkdirAll(fs, fmt.Sprintf("%s/cOS", constants.LiveDir), constants.DirPerm) @@ -107,7 +108,8 @@ var _ = Describe("Runtime Actions", func() { { Name: "device1", FilesystemLabel: "COS_GRUB", - Type: "ext4", + Type: "vfat", + MountPoint: constants.EfiDir, }, { Name: "device2", @@ -337,7 +339,7 @@ var _ = Describe("Runtime Actions", func() { err := upgrade.Run() Expect(err).ToNot(HaveOccurred()) - actualBytes, err := fs.ReadFile(filepath.Join(constants.RunningStateDir, "grub_oem_env")) + actualBytes, err := fs.ReadFile(filepath.Join(constants.EfiDir, "grub_oem_env")) Expect(err).To(BeNil()) expected := map[string]string{ diff --git a/pkg/bootloader/bootloader_suite_test.go b/pkg/bootloader/bootloader_suite_test.go index 6257be87340..e192d5e21f6 100644 --- a/pkg/bootloader/bootloader_suite_test.go +++ b/pkg/bootloader/bootloader_suite_test.go @@ -25,5 +25,5 @@ import ( func TestTypes(t *testing.T) { RegisterFailHandler(Fail) - RunSpecs(t, "booloader type test suite") + RunSpecs(t, "bootloader type test suite") } diff --git a/pkg/bootloader/grub.go b/pkg/bootloader/grub.go index 063f2528192..dbb261187de 100644 --- a/pkg/bootloader/grub.go +++ b/pkg/bootloader/grub.go @@ -27,16 +27,16 @@ import ( "github.com/rancher/elemental-toolkit/pkg/utils" efilib "github.com/canonical/go-efilib" + eleefi "github.com/rancher/elemental-toolkit/pkg/efi" ) const ( - grubPrefix = "/grub2" grubCfgFile = "grub.cfg" +) - grubEFICfgTmpl = "search --no-floppy --label --set=root %s" + - "\nset prefix=($root)%s" + - "\nconfigfile $prefix/" + grubCfgFile +var ( + defaultGrubPrefixes = []string{"/EFI/ELEMENTAL", "/EFI/BOOT"} ) func getGModulePatterns(module string) []string { @@ -57,7 +57,7 @@ type Grub struct { grubEfiImg string mokMngr string - grubPrefix string + grubPrefixes []string configFile string elementalCfg string disableBootEntry bool @@ -80,8 +80,8 @@ func NewGrub(cfg *v1.Config, opts ...GrubOptions) *Grub { logger: cfg.Logger, runner: cfg.Runner, platform: cfg.Platform, - grubPrefix: grubPrefix, configFile: grubCfgFile, + grubPrefixes: defaultGrubPrefixes, elementalCfg: filepath.Join(constants.GrubCfgPath, constants.GrubCfg), clearBootEntry: true, secureBoot: secureBoot, @@ -107,7 +107,7 @@ func WithSecureBoot(secureboot bool) func(g *Grub) error { func WithGrubPrefix(prefix string) func(g *Grub) error { return func(g *Grub) error { - g.grubPrefix = prefix + g.grubPrefixes = append(g.grubPrefixes, prefix) return nil } } @@ -174,16 +174,18 @@ func (g *Grub) installModules(rootDir, bootDir string, modules ...string) error if err != nil { return err } - for _, module := range modules { - fileWriteName := filepath.Join(bootDir, g.grubPrefix, fmt.Sprintf("%s-efi", g.platform.Arch), filepath.Base(module)) - g.logger.Debugf("Copying %s to %s", module, fileWriteName) - err = utils.MkdirAll(g.fs, filepath.Dir(fileWriteName), constants.DirPerm) - if err != nil { - return fmt.Errorf("error creating destination folder: %v", err) - } - err = utils.CopyFile(g.fs, module, fileWriteName) - if err != nil { - return fmt.Errorf("error copying %s to %s: %s", module, fileWriteName, err.Error()) + for _, grubPrefix := range g.grubPrefixes { + for _, module := range modules { + fileWriteName := filepath.Join(bootDir, grubPrefix, fmt.Sprintf("%s-efi", g.platform.Arch), filepath.Base(module)) + g.logger.Debugf("Copying %s to %s", module, fileWriteName) + err = utils.MkdirAll(g.fs, filepath.Dir(fileWriteName), constants.DirPerm) + if err != nil { + return fmt.Errorf("error creating destination folder: %v", err) + } + err = utils.CopyFile(g.fs, module, fileWriteName) + if err != nil { + return fmt.Errorf("error copying %s to %s: %s", module, fileWriteName, err.Error()) + } } } return nil @@ -208,15 +210,15 @@ func (g *Grub) InstallEFI(rootDir, bootDir, efiDir, deviceLabel string) error { return nil } -func (g *Grub) InstallEFIFallbackBinaries(rootDir, efiDir, deviceLabel string) error { - return g.installEFIPartitionBinaries(rootDir, efiDir, constants.FallbackEFIPath, deviceLabel) +func (g *Grub) InstallEFIFallbackBinaries(rootDir, efiDir, _ string) error { + return g.installEFIPartitionBinaries(rootDir, efiDir, constants.FallbackEFIPath) } -func (g *Grub) InstallEFIElementalBinaries(rootDir, efiDir, deviceLabel string) error { - return g.installEFIPartitionBinaries(rootDir, efiDir, constants.EntryEFIPath, deviceLabel) +func (g *Grub) InstallEFIElementalBinaries(rootDir, efiDir, _ string) error { + return g.installEFIPartitionBinaries(rootDir, efiDir, constants.EntryEFIPath) } -func (g *Grub) installEFIPartitionBinaries(rootDir, efiDir, efiPath, deviceLabel string) error { +func (g *Grub) installEFIPartitionBinaries(rootDir, efiDir, efiPath string) error { err := g.findEFIImages(rootDir) if err != nil { return err @@ -274,12 +276,6 @@ func (g *Grub) installEFIPartitionBinaries(rootDir, efiDir, efiPath, deviceLabel return fmt.Errorf("failed copying %s to %s: %s", g.grubEfiImg, installPath, err.Error()) } - grubCfgContent := []byte(fmt.Sprintf(grubEFICfgTmpl, deviceLabel, g.grubPrefix)) - err = g.fs.WriteFile(filepath.Join(efiDir, efiPath, grubCfgFile), grubCfgContent, constants.FilePerm) - if err != nil { - return fmt.Errorf("error writing %s: %s", filepath.Join(efiDir, efiPath, grubCfgFile), err) - } - return nil } @@ -402,8 +398,8 @@ func (g *Grub) SetDefaultEntry(partMountPoint, imgMountPoint, defaultEntry strin } // Install installs grub into the device, copy the config file and add any extra TTY to grub -func (g *Grub) Install(rootDir, bootDir, stateLabel string) (err error) { - err = g.InstallEFI(rootDir, bootDir, constants.EfiDir, stateLabel) +func (g *Grub) Install(rootDir, bootDir, deviceLabel string) (err error) { + err = g.InstallEFI(rootDir, bootDir, constants.EfiDir, deviceLabel) if err != nil { return err } @@ -422,24 +418,29 @@ func (g *Grub) Install(rootDir, bootDir, stateLabel string) (err error) { return g.InstallConfig(rootDir, bootDir) } -// InstallConfig installs grub configuraton files to the expected location. rootDir is the root -// of the OS image, bootDir is the folder grub read the configuration from, usually state partition mountpoint +// InstallConfig installs grub configuraton files to the expected location. +// rootDir is the root of the OS image, bootDir is the folder grub read the +// configuration from, usually EFI partition mountpoint func (g Grub) InstallConfig(rootDir, bootDir string) error { - grubFile := filepath.Join(rootDir, g.elementalCfg) - dstGrubFile := filepath.Join(bootDir, g.grubPrefix, g.configFile) + for _, path := range []string{constants.FallbackEFIPath, constants.EntryEFIPath} { + grubFile := filepath.Join(rootDir, g.elementalCfg) + dstGrubFile := filepath.Join(bootDir, path, g.configFile) - g.logger.Infof("Using grub config file %s", grubFile) + g.logger.Infof("Using grub config file %s", grubFile) - // Create Needed dir under state partition to store the grub.cfg and any needed modules - err := utils.MkdirAll(g.fs, filepath.Join(bootDir, g.grubPrefix), constants.DirPerm) - if err != nil { - return fmt.Errorf("error creating grub dir: %s", err) - } + // Create Needed dir under state partition to store the grub.cfg and any needed modules + err := utils.MkdirAll(g.fs, filepath.Join(bootDir, path), constants.DirPerm) + if err != nil { + return fmt.Errorf("error creating grub dir: %s", err) + } - g.logger.Infof("Copying grub config file from %s to %s", grubFile, dstGrubFile) - err = utils.CopyFile(g.fs, grubFile, dstGrubFile) - if err != nil { - g.logger.Errorf("Failed copying grub config file: %s", err) + g.logger.Infof("Copying grub config file from %s to %s", grubFile, dstGrubFile) + err = utils.CopyFile(g.fs, grubFile, dstGrubFile) + if err != nil { + g.logger.Errorf("Failed copying grub config file: %s", err) + return err + } } - return err + + return nil } diff --git a/pkg/bootloader/grub_test.go b/pkg/bootloader/grub_test.go index 58f4dd21cca..a8d54edda03 100644 --- a/pkg/bootloader/grub_test.go +++ b/pkg/bootloader/grub_test.go @@ -24,6 +24,7 @@ import ( efi "github.com/canonical/go-efilib" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "github.com/rancher/elemental-toolkit/cmd" "github.com/rancher/elemental-toolkit/pkg/bootloader" "github.com/rancher/elemental-toolkit/pkg/config" @@ -104,6 +105,7 @@ var _ = Describe("Booloader", Label("bootloader", "grub"), func() { config.WithLogger(logger), config.WithRunner(runner), config.WithFs(fs), + config.WithPlatform("linux/amd64"), ) }) @@ -111,15 +113,21 @@ var _ = Describe("Booloader", Label("bootloader", "grub"), func() { grub = bootloader.NewGrub(cfg, bootloader.WithGrubDisableBootEntry(true)) Expect(grub.Install(rootDir, bootDir, "DEVICE_LABEL")).To(Succeed()) - // Check everything is copied in boot directory - data, err := fs.ReadFile(fmt.Sprintf("%s/grub2/grub.cfg", bootDir)) + // Check grub config in EFI directory + data, err := fs.ReadFile(filepath.Join(bootDir, "/EFI/BOOT/grub.cfg")) + Expect(err).To(BeNil()) + Expect(data).To(Equal(grubCfg)) + + data, err = fs.ReadFile(filepath.Join(bootDir, "/EFI/ELEMENTAL/grub.cfg")) Expect(err).To(BeNil()) Expect(data).To(Equal(grubCfg)) - _, err = fs.Stat(fmt.Sprintf("%s/grub2/x86_64-efi/loopback.mod", bootDir)) + + // Check everything is copied in boot directory + _, err = fs.Stat(filepath.Join(bootDir, "EFI/BOOT/x86_64-efi/loopback.mod")) Expect(err).To(BeNil()) - _, err = fs.Stat(fmt.Sprintf("%s/grub2/x86_64-efi/xzio.mod", bootDir)) + _, err = fs.Stat(filepath.Join(bootDir, "EFI/BOOT/x86_64-efi/xzio.mod")) Expect(err).To(BeNil()) - _, err = fs.Stat(fmt.Sprintf("%s/grub2/x86_64-efi/squash4.mod", bootDir)) + _, err = fs.Stat(filepath.Join(bootDir, "EFI/BOOT/x86_64-efi/squash4.mod")) Expect(err).To(BeNil()) // Check everything is copied in EFI directory @@ -137,19 +145,16 @@ var _ = Describe("Booloader", Label("bootloader", "grub"), func() { Expect(err).To(BeNil()) }) - It("installs just fine without sercure boot", func() { + It("installs just fine without secure boot", func() { grub = bootloader.NewGrub(cfg, bootloader.WithGrubDisableBootEntry(true), bootloader.WithSecureBoot(false)) Expect(grub.Install(rootDir, bootDir, "DEVICE_LABEL")).To(Succeed()) // Check everything is copied in boot directory - data, err := fs.ReadFile(fmt.Sprintf("%s/grub2/grub.cfg", bootDir)) + _, err = fs.Stat(filepath.Join(bootDir, "EFI/BOOT/x86_64-efi/loopback.mod")) Expect(err).To(BeNil()) - Expect(data).To(Equal(grubCfg)) - _, err = fs.Stat(fmt.Sprintf("%s/grub2/x86_64-efi/loopback.mod", bootDir)) + _, err = fs.Stat(filepath.Join(bootDir, "EFI/BOOT/x86_64-efi/xzio.mod")) Expect(err).To(BeNil()) - _, err = fs.Stat(fmt.Sprintf("%s/grub2/x86_64-efi/xzio.mod", bootDir)) - Expect(err).To(BeNil()) - _, err = fs.Stat(fmt.Sprintf("%s/grub2/x86_64-efi/squash4.mod", bootDir)) + _, err = fs.Stat(filepath.Join(bootDir, "EFI/BOOT/x86_64-efi/squash4.mod")) Expect(err).To(BeNil()) // Check secureboot files are NOT there @@ -167,6 +172,15 @@ var _ = Describe("Booloader", Label("bootloader", "grub"), func() { Expect(err).To(BeNil()) _, err = fs.Stat(filepath.Join(constants.EfiDir, "EFI/ELEMENTAL/grub.efi")) Expect(err).To(BeNil()) + + // Check grub config in EFI directory + data, err := fs.ReadFile(filepath.Join(bootDir, "EFI/BOOT/grub.cfg")) + Expect(err).To(BeNil()) + Expect(data).To(Equal(grubCfg)) + + data, err = fs.ReadFile(filepath.Join(bootDir, "EFI/ELEMENTAL/grub.cfg")) + Expect(err).To(BeNil()) + Expect(data).To(Equal(grubCfg)) }) It("fails to install if squash4.mod is missing", func() { @@ -196,7 +210,7 @@ var _ = Describe("Booloader", Label("bootloader", "grub"), func() { Expect(grub.InstallConfig(rootDir, bootDir)).To(Succeed()) // Check everything is copied in boot directory - data, err := fs.ReadFile(fmt.Sprintf("%s/grub2/grub.cfg", bootDir)) + data, err := fs.ReadFile(filepath.Join(bootDir, "EFI/ELEMENTAL/grub.cfg")) Expect(err).To(BeNil()) Expect(data).To(Equal(grubCfg)) }) @@ -218,11 +232,11 @@ var _ = Describe("Booloader", Label("bootloader", "grub"), func() { Expect(grub.InstallEFI(rootDir, bootDir, efiDir, "DEVICE_LABEL")).To(Succeed()) // Check everything is copied in boot directory - _, err = fs.Stat(fmt.Sprintf("%s/grub2/x86_64-efi/loopback.mod", bootDir)) + _, err = fs.Stat(filepath.Join(bootDir, "EFI/BOOT/x86_64-efi/loopback.mod")) Expect(err).To(BeNil()) - _, err = fs.Stat(fmt.Sprintf("%s/grub2/x86_64-efi/xzio.mod", bootDir)) + _, err = fs.Stat(filepath.Join(bootDir, "EFI/BOOT/x86_64-efi/xzio.mod")) Expect(err).To(BeNil()) - _, err = fs.Stat(fmt.Sprintf("%s/grub2/x86_64-efi/squash4.mod", bootDir)) + _, err = fs.Stat(filepath.Join(bootDir, "EFI/BOOT/x86_64-efi/squash4.mod")) Expect(err).To(BeNil()) // Check everything is copied in EFI directory diff --git a/pkg/config/config.go b/pkg/config/config.go index 401f67099ce..f92fa348ec3 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -317,6 +317,9 @@ func NewInstallElementalPartitions() v1.ElementalPartitions { MountPoint: constants.PersistentDir, Flags: []string{}, } + + _ = partitions.SetFirmwarePartitions(v1.EFI, v1.GPT) + return partitions } diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index ba95277dfcf..fdb9799001a 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -134,12 +134,6 @@ var _ = Describe("Types", Label("types", "config"), func() { Expect(spec.Recovery.Source.Value()).To(Equal(spec.Active.File)) Expect(spec.PartTable).To(Equal(v1.GPT)) - // No firmware partitions added yet - Expect(spec.Partitions.EFI).To(BeNil()) - - // Adding firmware partitions - err = spec.Partitions.SetFirmwarePartitions(spec.Firmware, spec.PartTable) - Expect(err).ShouldNot(HaveOccurred()) Expect(spec.Partitions.EFI).NotTo(BeNil()) }) It("sets installation defaults without being on installation media", Label("install"), func() { diff --git a/pkg/elemental/elemental.go b/pkg/elemental/elemental.go index 9098aaf1daf..0a3abc645ee 100644 --- a/pkg/elemental/elemental.go +++ b/pkg/elemental/elemental.go @@ -171,14 +171,14 @@ func MountRWPartition(c v1.Config, part *v1.Partition) (umount func() error, err if mnt, _ := IsMounted(c, part); mnt { err = MountPartition(c, part, "remount", "rw") if err != nil { - c.Logger.Errorf("failed mounting %s partition: %v", part.Name, err) + c.Logger.Errorf("Failed mounting %s partition: %s", part.Name, err.Error()) return nil, err } umount = func() error { return MountPartition(c, part, "remount", "ro") } } else { err = MountPartition(c, part, "rw") if err != nil { - c.Logger.Error("failed mounting %s partition: %v", part.Name, err) + c.Logger.Errorf("Failed mounting %s partition: %s", part.Name, err.Error()) return nil, err } umount = func() error { return UnmountPartition(c, part) } diff --git a/pkg/elemental/elemental_test.go b/pkg/elemental/elemental_test.go index 1963a02e5d9..61586fc7999 100644 --- a/pkg/elemental/elemental_test.go +++ b/pkg/elemental/elemental_test.go @@ -178,6 +178,7 @@ var _ = Describe("Elemental", Label("elemental"), func() { _, err = fs.Create("/some/device") Expect(err).ToNot(HaveOccurred()) + parts.EFI.Path = "/dev/device1" parts.OEM.Path = "/dev/device2" parts.Recovery.Path = "/dev/device3" parts.State.Path = "/dev/device4" @@ -188,7 +189,7 @@ var _ = Describe("Elemental", Label("elemental"), func() { err := elemental.MountPartitions(*config, parts.PartitionsByMountPoint(false)) Expect(err).To(BeNil()) lst, _ := mounter.List() - Expect(len(lst)).To(Equal(4)) + Expect(len(lst)).To(Equal(5)) }) It("Mounts disk partitions excluding recovery", func() { @@ -223,6 +224,7 @@ var _ = Describe("Elemental", Label("elemental"), func() { _, err = fs.Create("/some/device") Expect(err).ToNot(HaveOccurred()) + parts.EFI.Path = "/dev/device1" parts.OEM.Path = "/dev/device2" parts.Recovery.Path = "/dev/device3" parts.State.Path = "/dev/device4" diff --git a/pkg/features/embedded/cloud-config-defaults/system/oem/03_branding.yaml b/pkg/features/embedded/cloud-config-defaults/system/oem/03_branding.yaml index 11746a3a084..d8206fc5b6b 100644 --- a/pkg/features/embedded/cloud-config-defaults/system/oem/03_branding.yaml +++ b/pkg/features/embedded/cloud-config-defaults/system/oem/03_branding.yaml @@ -9,7 +9,7 @@ name: "Branding" stages: initramfs: - name: "Branding" - if: '[ ! -f "/run/elemental/recovery_mode" ]' + if: '[ -f "/run/elemental/active_mode" ]' hostname: "elemental" files: - path: /etc/issue @@ -27,6 +27,25 @@ stages: permissions: 0644 owner: 0 group: 0 + - name: "Branding" + if: '[ -f "/run/elemental/passive_mode" ]' + hostname: "elemental" + files: + - path: /etc/issue + content: | + .-----. + | .-. | + | |.| | + | `-' | + `-----' + + Welcome to \S (passive)! + IP address \4 + Login with user: root, password: cos + There might be an issue with the active partition, booted in passive. + permissions: 0644 + owner: 0 + group: 0 - name: "Branding recovery" if: '[ -f "/run/elemental/recovery_mode" ]' hostname: "elemental" diff --git a/pkg/features/embedded/cloud-config-essentials/system/oem/08_boot_assessment.yaml b/pkg/features/embedded/cloud-config-essentials/system/oem/08_boot_assessment.yaml index 954114aea6c..347f2632e06 100644 --- a/pkg/features/embedded/cloud-config-essentials/system/oem/08_boot_assessment.yaml +++ b/pkg/features/embedded/cloud-config-essentials/system/oem/08_boot_assessment.yaml @@ -21,10 +21,10 @@ stages: cat /proc/cmdline | grep -q "active" commands: - | - mount -o rw,remount /run/initramfs/elemental-state - grub2-editenv /run/initramfs/elemental-state/boot_assessment set enable_boot_assessment= - grub2-editenv /run/initramfs/elemental-state/boot_assessment set boot_assessment_tentative= - mount -o ro,remount /run/initramfs/elemental-state + mount -o rw,remount /run/elemental/efi + grub2-editenv /run/elemental/efi/boot_assessment set enable_boot_assessment= + grub2-editenv /run/elemental/efi/boot_assessment set boot_assessment_tentative= + mount -o ro,remount /run/elemental/efi - name: "Create upgrade failure sentinel if necessary" if: | cat /proc/cmdline | grep -q "upgrade_failure" @@ -35,30 +35,29 @@ stages: owner: 0 group: 0 after-install: - # After install, reset, and upgrade, we install additional GRUB configuration for boot assessment into COS_STATE. + # After install, reset, and upgrade, we install additional GRUB configuration for boot assessment into COS_GRUB. - - &statemount - name: "Mount state" + - &efimount + name: "Mount efi" commands: - | - STATEDIR=/tmp/mnt/STATE - STATE=$(blkid -L COS_STATE || true) - mkdir -p $STATEDIR || true - mount ${STATE} $STATEDIR + EFIDIR=/tmp/mnt/EFI + EFI=$(blkid -L COS_GRUB || true) + mkdir -p $EFIDIR || true + mount ${EFI} $EFIDIR # Here we hook the boot assessment configuration to 'grubcustom' # we do that selectively in order to just "append" eventual other configuration provided. # XXX: maybe we should just write to /grubcustom and override any other custom grub? - &customhook name: "Hook boot assessment grub configuration" if: | - ! grep -q "grub_boot_assessment" /tmp/mnt/STATE/grubcustom + ! grep -q "grub_boot_assessment" /tmp/mnt/EFI/grubcustom commands: - | - cat << 'EOF' >> /tmp/mnt/STATE/grubcustom + cat << 'EOF' >> /tmp/mnt/EFI/grubcustom set bootfile="/grub_boot_assessment" - search --no-floppy --file --set=bootfile_loc "${bootfile}" - if [ "${bootfile_loc}" ]; then - source "(${bootfile_loc})${bootfile}" + if [ "${bootfile}" ]; then + source "${bootfile}" fi EOF # Overrides the active cmdline by adding "rd.emergency=reboot", "rd.shell=0" and "panic=5" @@ -72,10 +71,10 @@ stages: - &bootgrub name: "Add boot assessment grub configuration" files: - - path: "/tmp/mnt/STATE/grub_boot_assessment" + - path: "/tmp/mnt/EFI/grub_boot_assessment" owner: 0 group: 0 - permsisions: 0600 + permissions: 0600 content: | set extra_active_cmdline="rd.emergency=reboot rd.shell=0 panic=5 systemd.crash_reboot systemd.crash_shell=0" set boot_assessment_file="/boot_assessment" @@ -93,28 +92,33 @@ stages: fi fi fi - - &stateumount - name: "umount state" + - &efiumount + name: "umount EFI" commands: - | - umount /tmp/mnt/STATE + umount /tmp/mnt/EFI # Here we do enable boot assessment for the next bootup. # Similarly, we could trigger boot assessment in other cases after-upgrade: - - <<: *statemount + - <<: *efimount - name: "Set upgrade sentinel" commands: - | - grub2-editenv /tmp/mnt/STATE/boot_assessment set enable_boot_assessment=yes + grub2-editenv /tmp/mnt/EFI/boot_assessment set enable_boot_assessment=yes # We do re-install hooks here if needed to track upgrades of boot assessment - <<: *customhook - <<: *bootgrub - - <<: *stateumount + - <<: *efiumount after-reset: - - <<: *statemount + - <<: *efimount + - name: "Remove GRUB sentinels" + commands: + - | + grub2-editenv /tmp/mnt/EFI/boot_assessment set enable_boot_assessment= + grub2-editenv /tmp/mnt/EFI/boot_assessment set boot_assessment_tentative= # Reset completely restores COS_STATE, so we re-inject ourselves - <<: *customhook - <<: *bootgrub - - <<: *stateumount + - <<: *efiumount diff --git a/pkg/features/embedded/grub-config/etc/elemental/grub.cfg b/pkg/features/embedded/grub-config/etc/elemental/grub.cfg index 4d1ed6a88d4..a02afa377f7 100644 --- a/pkg/features/embedded/grub-config/etc/elemental/grub.cfg +++ b/pkg/features/embedded/grub-config/etc/elemental/grub.cfg @@ -46,6 +46,7 @@ insmod squash4 set loopdev="loop0" menuentry "${display_name}" --id cos { + search --no-floppy --label --set=root $state_label # label is kept around for backward compatibility set label=${active_label} set img=/cOS/active.img @@ -58,6 +59,7 @@ menuentry "${display_name}" --id cos { } menuentry "${display_name} (fallback)" --id fallback { + search --no-floppy --label --set=root $state_label # label is kept around for backward compatibility set label=${passive_label} set img=/cOS/passive.img diff --git a/pkg/types/v1/bootloader.go b/pkg/types/v1/bootloader.go index d1ffd5eb7e6..5e9edee53b1 100644 --- a/pkg/types/v1/bootloader.go +++ b/pkg/types/v1/bootloader.go @@ -17,7 +17,7 @@ limitations under the License. package v1 type Bootloader interface { - Install(rootDir, bootDir, stateLabel string) (err error) + Install(rootDir, bootDir, deviceLabel string) (err error) InstallConfig(rootDir, bootDir string) error DoEFIEntries(shimName, efiDir string) error InstallEFI(rootDir, bootDir, efiDir, deviceLabel string) error diff --git a/pkg/types/v1/config_test.go b/pkg/types/v1/config_test.go index 63defa849ee..fb8355ee806 100644 --- a/pkg/types/v1/config_test.go +++ b/pkg/types/v1/config_test.go @@ -380,7 +380,7 @@ var _ = Describe("Types", Label("types", "config"), func() { }) Describe("sanitize", func() { It("runs method", func() { - Expect(spec.Partitions.EFI).To(BeNil()) + Expect(spec.Partitions.EFI).ToNot(BeNil()) Expect(spec.Active.Source.IsEmpty()).To(BeTrue()) // Creates firmware partitions diff --git a/tests/fallback/fallback_test.go b/tests/fallback/fallback_test.go index 3aca45e67c1..47735e67d75 100644 --- a/tests/fallback/fallback_test.go +++ b/tests/fallback/fallback_test.go @@ -32,10 +32,10 @@ var _ = Describe("Elemental booting fallback tests", func() { var s *sut.SUT bootAssessmentInstalled := func() { // Auto assessment was installed - out, _ := s.Command("sudo cat /run/initramfs/elemental-state/grubcustom") - Expect(out).To(ContainSubstring("bootfile_loc")) + out, _ := s.Command("sudo cat /run/elemental/efi/grubcustom") + Expect(out).To(ContainSubstring("bootfile")) - out, _ = s.Command("sudo cat /run/initramfs/elemental-state/grub_boot_assessment") + out, _ = s.Command("sudo cat /run/elemental/efi/grub_boot_assessment") Expect(out).To(ContainSubstring("boot_assessment_file")) cmdline, _ := s.Command("sudo cat /proc/cmdline") @@ -69,7 +69,7 @@ var _ = Describe("Elemental booting fallback tests", func() { Expect(err).ToNot(HaveOccurred(), out) Expect(out).Should(ContainSubstring("Upgrade completed")) - out, _ = s.Command("sudo cat /run/initramfs/elemental-state/boot_assessment") + out, _ = s.Command("sudo cat /run/elemental/efi/boot_assessment") Expect(out).To(ContainSubstring("enable_boot_assessment=yes")) // Break the upgrade diff --git a/tests/recovery/recovery_test.go b/tests/recovery/recovery_test.go index 130e92dfe9b..2e84ad61f3f 100644 --- a/tests/recovery/recovery_test.go +++ b/tests/recovery/recovery_test.go @@ -50,7 +50,7 @@ var _ = Describe("Elemental Recovery upgrade tests", func() { Expect(s.ChangeBoot(sut.Recovery)).To(Succeed()) s.Reboot() - Expect(s.BootFrom()).To(Equal(sut.Recovery)) + s.EventuallyBootedFrom(sut.Recovery) By(fmt.Sprintf("upgrading to %s", comm.UpgradeImage())) @@ -66,7 +66,7 @@ var _ = Describe("Elemental Recovery upgrade tests", func() { Expect(err).ToNot(HaveOccurred()) s.Reboot() - ExpectWithOffset(1, s.BootFrom()).To(Equal(sut.Active)) + s.EventuallyBootedFrom(sut.Active) upgradedVersion := s.GetOSRelease("TIMESTAMP") Expect(upgradedVersion).ToNot(Equal(currentVersion)) @@ -94,11 +94,11 @@ var _ = Describe("Elemental Recovery upgrade tests", func() { // TODO: verify state.yaml matches expectations s.Reboot() - Expect(s.BootFrom()).To(Equal(sut.Recovery)) + s.EventuallyBootedFrom(sut.Recovery) By("rebooting back to active") s.Reboot() - Expect(s.BootFrom()).To(Equal(sut.Active)) + s.EventuallyBootedFrom(sut.Active) }) }) }) diff --git a/tests/upgrade/upgrade_test.go b/tests/upgrade/upgrade_test.go index f62b3db94e5..d54ddd57eb6 100644 --- a/tests/upgrade/upgrade_test.go +++ b/tests/upgrade/upgrade_test.go @@ -32,7 +32,7 @@ var _ = Describe("Elemental Feature tests", func() { BeforeEach(func() { s = sut.NewSUT() s.EventuallyConnects() - Expect(s.BootFrom()).To(Equal(sut.Active)) + s.EventuallyBootedFrom(sut.Active) }) Context("After install", func() { @@ -51,7 +51,7 @@ var _ = Describe("Elemental Feature tests", func() { Expect(out).Should(ContainSubstring("Upgrade completed")) s.Reboot() - Expect(s.BootFrom()).To(Equal(sut.Active)) + s.EventuallyBootedFrom(sut.Active) currentVersion := s.GetOSRelease("TIMESTAMP") Expect(currentVersion).NotTo(Equal(originalVersion)) diff --git a/tests/vm/sut.go b/tests/vm/sut.go index b3cc1d5e550..d5fd2c69688 100644 --- a/tests/vm/sut.go +++ b/tests/vm/sut.go @@ -188,7 +188,7 @@ func (s *SUT) Reset() { err := s.ChangeBootOnce(Recovery) Expect(err).ToNot(HaveOccurred()) s.Reboot() - Expect(s.BootFrom()).To(Equal(Recovery)) + s.EventuallyBootedFrom(Recovery) } By("Running elemental reset") @@ -220,6 +220,17 @@ func (s *SUT) BootFrom() string { } } +func (s *SUT) EventuallyBootedFrom(image string) { + Eventually(func() error { + actual := s.BootFrom() + if actual != image { + return fmt.Errorf("Expected boot from %s, actual %s", image, actual) + } + + return nil + }, time.Duration(60)*time.Second, time.Duration(10)*time.Second).ShouldNot(HaveOccurred()) +} + func (s *SUT) GetOSRelease(ss string) string { out, err := s.Command(fmt.Sprintf("source /etc/os-release && echo $%s", ss)) Expect(err).ToNot(HaveOccurred())