From b176b47f5609b2154a2d70efa7d6b78b9a38d7ae Mon Sep 17 00:00:00 2001 From: Itxaka Date: Fri, 7 Jun 2024 16:45:50 +0200 Subject: [PATCH] Copy any found sysextensions into active+passive efi dir (#372) --- internal/agent/hooks/hook.go | 1 + internal/agent/hooks/hooks_test.go | 187 +++++++++++++++++++++++++++++ internal/agent/hooks/sysext.go | 88 ++++++++++++++ pkg/utils/fs/fs.go | 30 +++++ 4 files changed, 306 insertions(+) create mode 100644 internal/agent/hooks/hooks_test.go create mode 100644 internal/agent/hooks/sysext.go diff --git a/internal/agent/hooks/hook.go b/internal/agent/hooks/hook.go index fef2e56f..4b96b3fe 100644 --- a/internal/agent/hooks/hook.go +++ b/internal/agent/hooks/hook.go @@ -33,6 +33,7 @@ var FirstBoot = []Interface{ // AfterUkiInstall sets which Hooks to run after uki runs the install action var AfterUkiInstall = []Interface{ + &SysExtPostInstall{}, &Lifecycle{}, } diff --git a/internal/agent/hooks/hooks_test.go b/internal/agent/hooks/hooks_test.go new file mode 100644 index 00000000..d91bfb77 --- /dev/null +++ b/internal/agent/hooks/hooks_test.go @@ -0,0 +1,187 @@ +package hook_test + +import ( + "bytes" + "github.com/jaypipes/ghw/pkg/block" + _ "github.com/kairos-io/kairos-agent/v2/internal/agent/hooks" + hook "github.com/kairos-io/kairos-agent/v2/internal/agent/hooks" + "github.com/kairos-io/kairos-agent/v2/pkg/config" + cnst "github.com/kairos-io/kairos-agent/v2/pkg/constants" + fsutils "github.com/kairos-io/kairos-agent/v2/pkg/utils/fs" + v1mock "github.com/kairos-io/kairos-agent/v2/tests/mocks" + "github.com/kairos-io/kairos-sdk/collector" + sdkTypes "github.com/kairos-io/kairos-sdk/types" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/twpayne/go-vfs/v4" + "github.com/twpayne/go-vfs/v4/vfst" + "os" + "path/filepath" + "testing" +) + +func TestConfig(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Hooks Suite") +} + +var _ = Describe("Hooks", func() { + var cfg *config.Config + var fs vfs.FS + var logger sdkTypes.KairosLogger + var runner *v1mock.FakeRunner + var mounter *v1mock.ErrorMounter + var syscallMock *v1mock.FakeSyscall + var client *v1mock.FakeHTTPClient + var cloudInit *v1mock.FakeCloudInitRunner + var cleanup func() + var memLog *bytes.Buffer + var extractor *v1mock.FakeImageExtractor + var ghwTest v1mock.GhwMock + var err error + + Context("SysExtPostInstall", func() { + BeforeEach(func() { + runner = v1mock.NewFakeRunner() + syscallMock = &v1mock.FakeSyscall{} + mounter = v1mock.NewErrorMounter() + client = &v1mock.FakeHTTPClient{} + memLog = &bytes.Buffer{} + logger = sdkTypes.NewBufferLogger(memLog) + extractor = v1mock.NewFakeImageExtractor(logger) + logger.SetLevel("debug") + fs, cleanup, err = vfst.NewTestFS(map[string]interface{}{}) + // Create proper dir structure for our EFI partition contents + Expect(err).Should(BeNil()) + err = fsutils.MkdirAll(fs, "/efi/loader/entries", os.ModeDir|os.ModePerm) + Expect(err).Should(BeNil()) + err = fsutils.MkdirAll(fs, "/efi/EFI/BOOT", os.ModeDir|os.ModePerm) + Expect(err).Should(BeNil()) + err = fsutils.MkdirAll(fs, "/efi/EFI/kairos", os.ModeDir|os.ModePerm) + Expect(err).Should(BeNil()) + err = fsutils.MkdirAll(fs, "/etc/cos/", os.ModeDir|os.ModePerm) + Expect(err).Should(BeNil()) + err = fsutils.MkdirAll(fs, "/run/initramfs/cos-state/grub/", os.ModeDir|os.ModePerm) + Expect(err).Should(BeNil()) + err = fsutils.MkdirAll(fs, "/etc/kairos/branding/", os.ModeDir|os.ModePerm) + Expect(err).Should(BeNil()) + + cloudInit = &v1mock.FakeCloudInitRunner{} + cfg = config.NewConfig( + config.WithFs(fs), + config.WithRunner(runner), + config.WithLogger(logger), + config.WithMounter(mounter), + config.WithSyscall(syscallMock), + config.WithClient(client), + config.WithCloudInitRunner(cloudInit), + config.WithImageExtractor(extractor), + ) + cfg.Config = collector.Config{} + + mainDisk := block.Disk{ + Name: "device", + Partitions: []*block.Partition{ + { + Name: "device1", + FilesystemLabel: "COS_GRUB", + Type: "ext4", + MountPoint: "/efi", + }, + }, + } + ghwTest = v1mock.GhwMock{} + ghwTest.AddDisk(mainDisk) + ghwTest.CreateDevices() + }) + AfterEach(func() { + cleanup() + }) + It("should copy all files with .sysext.raw extension", func() { + err = fsutils.MkdirAll(fs, cnst.LiveDir, os.ModeDir|os.ModePerm) + Expect(err).Should(BeNil()) + err = fs.WriteFile(filepath.Join(cnst.LiveDir, "test1.sysext.raw"), []byte("test"), os.ModePerm) + Expect(err).Should(BeNil()) + err = fs.WriteFile(filepath.Join(cnst.LiveDir, "test2.sysext.raw"), []byte("test"), os.ModePerm) + Expect(err).Should(BeNil()) + postInstall := hook.SysExtPostInstall{} + err = postInstall.Run(*cfg, nil) + Expect(err).Should(BeNil()) + // we expect them to be here as its where we mount the efi partition but then we fake unmount + _, err = fs.Stat(filepath.Join(cnst.EfiDir, "EFI/kairos/active.efi.extra.d/", "test1.sysext.raw")) + Expect(err).Should(BeNil()) + _, err = fs.Stat(filepath.Join(cnst.EfiDir, "EFI/kairos/active.efi.extra.d/", "test2.sysext.raw")) + Expect(err).Should(BeNil()) + }) + It("should ignore files without .sysext.raw extension", func() { + err = fsutils.MkdirAll(fs, cnst.LiveDir, os.ModeDir|os.ModePerm) + Expect(err).Should(BeNil()) + err = fs.WriteFile(filepath.Join(cnst.LiveDir, "test1.sysext.raw"), []byte("test"), os.ModePerm) + Expect(err).Should(BeNil()) + err = fs.WriteFile(filepath.Join(cnst.LiveDir, "test2.sysext.raw"), []byte("test"), os.ModePerm) + Expect(err).Should(BeNil()) + err = fs.WriteFile(filepath.Join(cnst.LiveDir, "hello.raw"), []byte("test"), os.ModePerm) + Expect(err).Should(BeNil()) + err = fs.WriteFile(filepath.Join(cnst.LiveDir, "hello.sysext.what.raw"), []byte("test"), os.ModePerm) + Expect(err).Should(BeNil()) + err = fs.WriteFile(filepath.Join(cnst.LiveDir, "hello.sysext"), []byte("test"), os.ModePerm) + Expect(err).Should(BeNil()) + postInstall := hook.SysExtPostInstall{} + err = postInstall.Run(*cfg, nil) + Expect(err).Should(BeNil()) + // we expect them to be here as its where we mount the efi partition but then we fake unmount + _, err = fs.Stat(filepath.Join(cnst.EfiDir, "EFI/kairos/active.efi.extra.d/", "test1.sysext.raw")) + Expect(err).Should(BeNil()) + _, err = fs.Stat(filepath.Join(cnst.EfiDir, "EFI/kairos/active.efi.extra.d/", "test2.sysext.raw")) + Expect(err).Should(BeNil()) + _, err = fs.Stat(filepath.Join(cnst.EfiDir, "EFI/kairos/active.efi.extra.d/", "hello.raw")) + Expect(err).ShouldNot(BeNil()) + _, err = fs.Stat(filepath.Join(cnst.EfiDir, "EFI/kairos/active.efi.extra.d/", "hello.sysext.what.raw")) + Expect(err).ShouldNot(BeNil()) + _, err = fs.Stat(filepath.Join(cnst.EfiDir, "EFI/kairos/active.efi.extra.d/", "hello.sysext")) + Expect(err).ShouldNot(BeNil()) + }) + It("doesn't error if it cant find the efi partition", func() { + ghwTest.Clean() + postInstall := hook.SysExtPostInstall{} + err = postInstall.Run(*cfg, nil) + Expect(err).Should(BeNil()) + }) + It("errors if it cant mount the efi partition and strict is set", func() { + ghwTest.Clean() + cfg.FailOnBundleErrors = true + postInstall := hook.SysExtPostInstall{} + err = postInstall.Run(*cfg, nil) + Expect(err).ShouldNot(BeNil()) + }) + It("doesn't error if it cant mount the efi partition", func() { + mounter.ErrorOnMount = true + postInstall := hook.SysExtPostInstall{} + err = postInstall.Run(*cfg, nil) + Expect(err).Should(BeNil()) + }) + It("errors if it cant mount the efi partition and strict is set", func() { + mounter.ErrorOnMount = true + cfg.FailOnBundleErrors = true + postInstall := hook.SysExtPostInstall{} + err = postInstall.Run(*cfg, nil) + Expect(err).ShouldNot(BeNil()) + }) + It("doesn't error if it cant create the dirs", func() { + ROfs := vfs.NewReadOnlyFS(fs) + cfg.Fs = ROfs + postInstall := hook.SysExtPostInstall{} + err = postInstall.Run(*cfg, nil) + Expect(err).Should(BeNil()) + }) + It("errors if it cant create the dirs and strict is set", func() { + cfg.FailOnBundleErrors = true + ROfs := vfs.NewReadOnlyFS(fs) + cfg.Fs = ROfs + postInstall := hook.SysExtPostInstall{} + err = postInstall.Run(*cfg, nil) + Expect(err).ShouldNot(BeNil()) + }) + + }) +}) diff --git a/internal/agent/hooks/sysext.go b/internal/agent/hooks/sysext.go new file mode 100644 index 00000000..6fd797ac --- /dev/null +++ b/internal/agent/hooks/sysext.go @@ -0,0 +1,88 @@ +package hook + +import ( + "github.com/kairos-io/kairos-agent/v2/pkg/config" + "github.com/kairos-io/kairos-agent/v2/pkg/constants" + "github.com/kairos-io/kairos-agent/v2/pkg/types/v1" + fsutils "github.com/kairos-io/kairos-agent/v2/pkg/utils/fs" + "github.com/kairos-io/kairos-agent/v2/pkg/utils/partitions" + "io/fs" + "path/filepath" + "strings" +) + +type SysExtPostInstall struct{} + +func (b SysExtPostInstall) Run(c config.Config, _ v1.Spec) error { + c.Logger.Logger.Debug().Msg("Running SysExtPostInstall hook") + // mount efi partition + efiPart, err := partitions.GetEfiPartition() + if err != nil { + c.Logger.Errorf("failed to get EFI partition: %s", err) + if c.FailOnBundleErrors { + return err + } + return nil + } + mounted, _ := c.Mounter.IsMountPoint(constants.EfiDir) + + if !mounted { + err = c.Mounter.Mount(efiPart.Path, constants.EfiDir, efiPart.FS, []string{"rw"}) + if err != nil { + c.Logger.Errorf("failed to mount EFI partition: %s", err) + if c.FailOnBundleErrors { + return err + } + return nil + } + defer func() { + _ = c.Mounter.Unmount(constants.EfiDir) + }() + } else { + // If its mounted, try to remount it RW + err = c.Mounter.Mount(efiPart.Path, constants.EfiDir, efiPart.FS, []string{"remount,rw"}) + defer func() { + _ = c.Mounter.Unmount(constants.EfiDir) + }() + } + + activeDir := filepath.Join(constants.EfiDir, "EFI/kairos/active.efi.extra.d/") + passiveDir := filepath.Join(constants.EfiDir, "EFI/kairos/passive.efi.extra.d/") + for _, dir := range []string{activeDir, passiveDir} { + err = fsutils.MkdirAll(c.Fs, dir, 0755) + if err != nil { + c.Logger.Errorf("failed to create directory %s: %s", dir, err) + if c.FailOnBundleErrors { + return err + } + return nil + } + } + + err = fsutils.WalkDirFs(c.Fs, constants.LiveDir, func(path string, info fs.DirEntry, err error) error { + if err != nil { + return err + } + if info.IsDir() { + return nil + } + if strings.HasSuffix(info.Name(), ".sysext.raw") { + // copy it to /EFI/Kairos/{active,passive}.efi.extra.d/ + err = fsutils.Copy(c.Fs, path, filepath.Join(activeDir, info.Name())) + if err != nil { + c.Logger.Errorf("failed to copy %s to %s: %s", path, activeDir, err) + if c.FailOnBundleErrors { + return err + } + return nil + } + c.Logger.Debugf("copied %s to %s", path, activeDir) + } + return nil + }) + if c.FailOnBundleErrors && err != nil { + return err + } + c.Logger.Logger.Debug().Msg("Done SysExtPostInstall hook") + return nil +} diff --git a/pkg/utils/fs/fs.go b/pkg/utils/fs/fs.go index 06eeb871..4d69aa12 100644 --- a/pkg/utils/fs/fs.go +++ b/pkg/utils/fs/fs.go @@ -21,6 +21,7 @@ package fsutils import ( "errors" + "io" "io/fs" "os" "path/filepath" @@ -243,3 +244,32 @@ func readDir(fs v1.FS, dirname string) ([]fs.DirEntry, error) { sort.Slice(dirs, func(i, j int) bool { return dirs[i].Name() < dirs[j].Name() }) return dirs, nil } + +// Copy copies src to dst like the cp command. +func Copy(fs v1.FS, src, dst string) error { + if dst == src { + return os.ErrInvalid + } + + srcF, err := fs.Open(src) + if err != nil { + return err + } + defer srcF.Close() + + info, err := srcF.Stat() + if err != nil { + return err + } + + dstF, err := fs.OpenFile(dst, os.O_RDWR|os.O_CREATE|os.O_TRUNC, info.Mode()) + if err != nil { + return err + } + defer dstF.Close() + + if _, err := io.Copy(dstF, srcF); err != nil { + return err + } + return nil +}