From 16924f648e4d3b533c42e4e901ec271be687e5b9 Mon Sep 17 00:00:00 2001 From: Noel Georgi Date: Fri, 24 Jan 2025 19:57:08 +0530 Subject: [PATCH] feat: generate iso's with both UKI and grub Starting with Talos 1.10, the default generated ISO's will use GRUB for BIOS boot and sd-boot for EFI boot. Fixes: #10192 Signed-off-by: Noel Georgi --- .github/workflows/ci.yaml | 12 ++- .../workflows/integration-misc-2-cron.yaml | 12 ++- .kres.yaml | 10 ++ hack/test/e2e-qemu.sh | 1 + pkg/imager/imager.go | 4 +- pkg/imager/iso/grub.go | 82 ++++---------- pkg/imager/iso/hybrid.go | 43 ++++++++ pkg/imager/iso/iso.go | 84 ++++++++++++++- pkg/imager/iso/uefi.go | 101 +++++++++--------- pkg/imager/out.go | 53 +++++++-- 10 files changed, 271 insertions(+), 131 deletions(-) create mode 100644 pkg/imager/iso/hybrid.go diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index a81677fceb7..6c71f0e12b7 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1,6 +1,6 @@ # THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT. # -# Generated on 2025-01-22T17:37:55Z by kres 3075de9. +# Generated on 2025-01-24T14:30:35Z by kres 3075de9. name: default concurrency: @@ -2199,6 +2199,16 @@ jobs: WITH_UEFI: "false" run: | sudo -E make e2e-qemu + - name: e2e-bios-iso + env: + GITHUB_STEP_NAME: ${{ github.job}}-e2e-bios-iso + IMAGE_REGISTRY: registry.dev.siderolabs.io + SHORT_INTEGRATION_TEST: "yes" + VIA_MAINTENANCE_MODE: "true" + WITH_ISO: "true" + WITH_UEFI: "false" + run: | + sudo -E make e2e-qemu - name: e2e-disk-image env: GITHUB_STEP_NAME: ${{ github.job}}-e2e-disk-image diff --git a/.github/workflows/integration-misc-2-cron.yaml b/.github/workflows/integration-misc-2-cron.yaml index b2eab7265ac..52a098ffde1 100644 --- a/.github/workflows/integration-misc-2-cron.yaml +++ b/.github/workflows/integration-misc-2-cron.yaml @@ -1,6 +1,6 @@ # THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT. # -# Generated on 2024-12-18T13:55:17Z by kres b9507d6. +# Generated on 2025-01-24T14:30:35Z by kres 3075de9. name: integration-misc-2-cron concurrency: @@ -99,6 +99,16 @@ jobs: WITH_UEFI: "false" run: | sudo -E make e2e-qemu + - name: e2e-bios-iso + env: + GITHUB_STEP_NAME: ${{ github.job}}-e2e-bios-iso + IMAGE_REGISTRY: registry.dev.siderolabs.io + SHORT_INTEGRATION_TEST: "yes" + VIA_MAINTENANCE_MODE: "true" + WITH_ISO: "true" + WITH_UEFI: "false" + run: | + sudo -E make e2e-qemu - name: e2e-disk-image env: GITHUB_STEP_NAME: ${{ github.job}}-e2e-disk-image diff --git a/.kres.yaml b/.kres.yaml index 94116f41efe..4ccff8762ea 100644 --- a/.kres.yaml +++ b/.kres.yaml @@ -838,6 +838,16 @@ spec: SHORT_INTEGRATION_TEST: yes WITH_UEFI: false IMAGE_REGISTRY: registry.dev.siderolabs.io + - name: e2e-bios-iso + command: e2e-qemu + withSudo: true + environment: + GITHUB_STEP_NAME: ${{ github.job}}-e2e-bios-iso + SHORT_INTEGRATION_TEST: yes + WITH_UEFI: false + VIA_MAINTENANCE_MODE: true + WITH_ISO: true + IMAGE_REGISTRY: registry.dev.siderolabs.io - name: e2e-disk-image command: e2e-qemu withSudo: true diff --git a/hack/test/e2e-qemu.sh b/hack/test/e2e-qemu.sh index 3b52dec9401..1758d18d2db 100755 --- a/hack/test/e2e-qemu.sh +++ b/hack/test/e2e-qemu.sh @@ -135,6 +135,7 @@ case "${WITH_ISO:-false}" in false) ;; *) + INSTALLER_IMAGE=${INSTALLER_IMAGE}-amd64-secureboot # we don't use secureboot part here, but this installer contains UKIs QEMU_FLAGS+=("--iso-path=${ARTIFACTS}/metal-amd64.iso") ;; esac diff --git a/pkg/imager/imager.go b/pkg/imager/imager.go index 4bbbc88cfae..fc26be3df20 100644 --- a/pkg/imager/imager.go +++ b/pkg/imager/imager.go @@ -111,9 +111,9 @@ func (i *Imager) Execute(ctx context.Context, outputPath string, report *reporte if !needBuildUKI { return "", fmt.Errorf("UKI output is not supported in this Talos version") } - case profile.OutKindISO, profile.OutKindImage: + case profile.OutKindImage: needBuildUKI = needBuildUKI && i.prof.SecureBootEnabled() - case profile.OutKindInstaller: + case profile.OutKindISO, profile.OutKindInstaller: needBuildUKI = needBuildUKI || quirks.New(i.prof.Version).UseSDBootForUEFI() case profile.OutKindCmdline, profile.OutKindKernel, profile.OutKindInitramfs: needBuildUKI = false diff --git a/pkg/imager/iso/grub.go b/pkg/imager/iso/grub.go index 12de88a8238..c125a4a6e73 100644 --- a/pkg/imager/iso/grub.go +++ b/pkg/imager/iso/grub.go @@ -7,44 +7,28 @@ package iso import ( "bytes" _ "embed" - "fmt" "os" "path/filepath" "text/template" - "time" - - "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/utils" "github.com/siderolabs/talos/pkg/machinery/imager/quirks" ) -// GRUBOptions described the input for the CreateGRUB function. -type GRUBOptions struct { - KernelPath string - InitramfsPath string - Cmdline string - Version string - - ScratchDir string - - OutPath string -} - //go:embed grub.cfg var grubCfgTemplate string // CreateGRUB creates a GRUB-based ISO image. // // This iso supports both BIOS and UEFI booting. -func CreateGRUB(printf func(string, ...any), options GRUBOptions) error { +func (options Options) CreateGRUB(printf func(string, ...any)) (Generator, error) { if err := utils.CopyFiles( printf, utils.SourceDestination(options.KernelPath, filepath.Join(options.ScratchDir, "boot", "vmlinuz")), utils.SourceDestination(options.InitramfsPath, filepath.Join(options.ScratchDir, "boot", "initramfs.xz")), ); err != nil { - return err + return nil, err } printf("creating grub.cfg") @@ -57,7 +41,7 @@ func CreateGRUB(printf func(string, ...any), options GRUBOptions) error { }). Parse(grubCfgTemplate) if err != nil { - return err + return nil, err } if err = tmpl.Execute(&grubCfg, struct { @@ -67,64 +51,34 @@ func CreateGRUB(printf func(string, ...any), options GRUBOptions) error { Cmdline: options.Cmdline, AddResetOption: quirks.New(options.Version).SupportsResetGRUBOption(), }); err != nil { - return err + return nil, err } cfgPath := filepath.Join(options.ScratchDir, "boot/grub/grub.cfg") if err = os.MkdirAll(filepath.Dir(cfgPath), 0o755); err != nil { - return err + return nil, err } if err = os.WriteFile(cfgPath, grubCfg.Bytes(), 0o666); err != nil { - return err + return nil, err } if err = utils.TouchFiles(printf, options.ScratchDir); err != nil { - return err + return nil, err } printf("creating ISO image") - return grubMkrescue(options) -} - -func grubMkrescue(options GRUBOptions) error { - args := []string{ - "--compress=xz", - "--output=" + options.OutPath, - "--verbose", - options.ScratchDir, - "--", - } - - if epoch, ok, err := utils.SourceDateEpoch(); err != nil { - return err - } else if ok { - // set EFI FAT image serial number - if err := os.Setenv("GRUB_FAT_SERIAL_NUMBER", fmt.Sprintf("%x", uint32(epoch))); err != nil { - return err - } - - args = append(args, - "-volume_date", "all_file_dates", fmt.Sprintf("=%d", epoch), - "-volume_date", "uuid", time.Unix(epoch, 0).Format("2006010215040500"), - ) - } - - if quirks.New(options.Version).SupportsISOLabel() { - label := Label(options.Version, false) - - args = append(args, - "-volid", VolumeID(label), - "-volset-id", label, - ) - } - - _, err := cmd.Run("grub-mkrescue", args...) - if err != nil { - return fmt.Errorf("failed to create ISO: %w", err) - } - - return nil + return &ExecutorOptions{ + Command: "grub-mkrescue", + Version: options.Version, + Arguments: []string{ + "--compress=xz", + "--output=" + options.OutPath, + "--verbose", + options.ScratchDir, + "--", + }, + }, nil } diff --git a/pkg/imager/iso/hybrid.go b/pkg/imager/iso/hybrid.go new file mode 100644 index 00000000000..29c8c4f44be --- /dev/null +++ b/pkg/imager/iso/hybrid.go @@ -0,0 +1,43 @@ +// 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 iso + +import "path/filepath" + +// CreateHybrid creates an ISO image that supports both BIOS and UEFI booting. +func (options Options) CreateHybrid(printf func(string, ...any)) (Generator, error) { + if _, err := options.CreateGRUB(printf); err != nil { + return nil, err + } + + if _, err := options.CreateUEFI(printf); err != nil { + return nil, err + } + + efiBootImg := filepath.Join(options.ScratchDir, "efiboot.img") + + return &ExecutorOptions{ + Command: "grub-mkrescue", + Version: options.Version, + Arguments: []string{ + "--compress=xz", + "--output=" + options.OutPath, + "--verbose", + "--directory=/usr/lib/grub/i386-pc", // only for BIOS boot + "-m", "efiboot.img", // exclude the EFI boot image from the ISO + options.ScratchDir, + "-eltorito-alt-boot", + "-e", "--interval:appended_partition_2:all::", // use appended partition 2 for EFI + "-append_partition", "2", "0xef", efiBootImg, + "-appended_part_as_gpt", + "-partition_cyl_align", // pad partition to cylinder boundary + "all", + "-partition_offset", "16", // support booting from USB + "-iso_mbr_part_type", "0x83", // just to have more clear info when doing a fdisk -l + "-no-emul-boot", + "--", + }, + }, nil +} diff --git a/pkg/imager/iso/iso.go b/pkg/imager/iso/iso.go index 076fea6ddde..5c10c6cdfab 100644 --- a/pkg/imager/iso/iso.go +++ b/pkg/imager/iso/iso.go @@ -5,7 +5,17 @@ // Package iso contains functions for creating ISO images. package iso -import "strings" +import ( + "fmt" + "os" + "strings" + "time" + + "github.com/siderolabs/go-cmd/pkg/cmd" + + "github.com/siderolabs/talos/pkg/imager/utils" + "github.com/siderolabs/talos/pkg/machinery/imager/quirks" +) // VolumeID returns a valid volume ID for the given label. func VolumeID(label string) string { @@ -41,3 +51,75 @@ func Label(version string, secureboot bool) string { return label + version } + +// ExecutorOptions defines the iso generation options. +type ExecutorOptions struct { + Command string + Version string + Arguments []string +} + +// Generator is an interface for executing the iso generation. +type Generator interface { + Generate() error +} + +// Options describe the input generating different types of ISOs. +type Options struct { + KernelPath string + InitramfsPath string + Cmdline string + + UKIPath string + SDBootPath string + + Arch string + Version string + + // A value in loader.conf secure-boot-enroll: off, manual, if-safe, force. + SDBootSecureBootEnrollKeys string + + // UKISigningCertDer is the DER encoded UKI signing certificate. + UKISigningCertDerPath string + + // optional, for auto-enrolling secureboot keys + PlatformKeyPath string + KeyExchangeKeyPath string + SignatureKeyPath string + + ScratchDir string + OutPath string +} + +// Generate creates an ISO image. +func (e *ExecutorOptions) Generate() error { + if epoch, ok, err := utils.SourceDateEpoch(); err != nil { + return err + } else if ok { + // set EFI FAT image serial number + if err := os.Setenv("GRUB_FAT_SERIAL_NUMBER", fmt.Sprintf("%x", uint32(epoch))); err != nil { + return err + } + + e.Arguments = append(e.Arguments, + "-volume_date", "all_file_dates", fmt.Sprintf("=%d", epoch), + "-volume_date", "uuid", time.Unix(epoch, 0).Format("2006010215040500"), + ) + } + + if quirks.New(e.Version).SupportsISOLabel() { + label := Label(e.Version, false) + + e.Arguments = append(e.Arguments, + "-volid", VolumeID(label), + "-volset-id", label, + ) + } + + _, err := cmd.Run(e.Command, e.Arguments...) + if err != nil { + return fmt.Errorf("failed to create ISO: %w", err) + } + + return nil +} diff --git a/pkg/imager/iso/uefi.go b/pkg/imager/iso/uefi.go index 15418a1a3cd..5e319a70bdd 100644 --- a/pkg/imager/iso/uefi.go +++ b/pkg/imager/iso/uefi.go @@ -17,7 +17,6 @@ import ( "github.com/siderolabs/talos/pkg/imager/utils" "github.com/siderolabs/talos/pkg/machinery/constants" - "github.com/siderolabs/talos/pkg/machinery/imager/quirks" "github.com/siderolabs/talos/pkg/makefs" ) @@ -57,9 +56,9 @@ var loaderConfigTemplate string // The ISO created supports only booting in UEFI mode, and supports SecureBoot. // //nolint:gocyclo,cyclop -func CreateUEFI(printf func(string, ...any), options UEFIOptions) error { +func (options Options) CreateUEFI(printf func(string, ...any)) (Generator, error) { if err := os.MkdirAll(options.ScratchDir, 0o755); err != nil { - return err + return nil, err } printf("preparing raw image") @@ -75,14 +74,14 @@ func CreateUEFI(printf func(string, ...any), options UEFIOptions) error { } { st, err := os.Stat(path) if err != nil { - return err + return nil, err } isoSize += (st.Size() + mib - 1) / mib * mib } if err := utils.CreateRawDisk(printf, efiBootImg, isoSize); err != nil { - return err + return nil, err } printf("preparing loader.conf") @@ -94,7 +93,7 @@ func CreateUEFI(printf func(string, ...any), options UEFIOptions) error { }{ SecureBootEnroll: options.SDBootSecureBootEnrollKeys, }); err != nil { - return fmt.Errorf("error rendering loader.conf: %w", err) + return nil, fmt.Errorf("error rendering loader.conf: %w", err) } printf("creating vFAT EFI image") @@ -105,23 +104,19 @@ func CreateUEFI(printf func(string, ...any), options UEFIOptions) error { } if err := makefs.VFAT(efiBootImg, fopts...); err != nil { - return err + return nil, err } if err := os.MkdirAll(filepath.Join(options.ScratchDir, "EFI/Linux"), 0o755); err != nil { - return err + return nil, err } if err := os.MkdirAll(filepath.Join(options.ScratchDir, "EFI/BOOT"), 0o755); err != nil { - return err + return nil, err } - if err := os.MkdirAll(filepath.Join(options.ScratchDir, "EFI/keys"), 0o755); err != nil { - return err - } - - if err := os.MkdirAll(filepath.Join(options.ScratchDir, "loader/keys/auto"), 0o755); err != nil { - return err + if err := os.MkdirAll(filepath.Join(options.ScratchDir, "loader"), 0o755); err != nil { + return nil, err } efiBootPath := "EFI/BOOT/BOOTX64.EFI" @@ -131,36 +126,48 @@ func CreateUEFI(printf func(string, ...any), options UEFIOptions) error { } if err := copy.File(options.SDBootPath, filepath.Join(options.ScratchDir, efiBootPath)); err != nil { - return err + return nil, err } if err := copy.File(options.UKIPath, filepath.Join(options.ScratchDir, fmt.Sprintf("EFI/Linux/Talos-%s.efi", options.Version))); err != nil { - return err + return nil, err } if err := os.WriteFile(filepath.Join(options.ScratchDir, "loader/loader.conf"), loaderConfigOut.Bytes(), 0o644); err != nil { - return err + return nil, err + } + + if options.UKISigningCertDerPath != "" { + if err := os.MkdirAll(filepath.Join(options.ScratchDir, "EFI/keys"), 0o755); err != nil { + return nil, err + } + + if err := copy.File(options.UKISigningCertDerPath, filepath.Join(options.ScratchDir, "EFI/keys/uki-signing-cert.der")); err != nil { + return nil, err + } } - if err := copy.File(options.UKISigningCertDerPath, filepath.Join(options.ScratchDir, "EFI/keys/uki-signing-cert.der")); err != nil { - return err + if options.PlatformKeyPath != "" || options.KeyExchangeKeyPath != "" || options.SignatureKeyPath != "" { + if err := os.MkdirAll(filepath.Join(options.ScratchDir, "loader/keys/auto"), 0o755); err != nil { + return nil, err + } } if options.PlatformKeyPath != "" { if err := copy.File(options.PlatformKeyPath, filepath.Join(options.ScratchDir, "loader/keys/auto", constants.PlatformKeyAsset)); err != nil { - return err + return nil, err } } if options.KeyExchangeKeyPath != "" { if err := copy.File(options.KeyExchangeKeyPath, filepath.Join(options.ScratchDir, "loader/keys/auto", constants.KeyExchangeKeyAsset)); err != nil { - return err + return nil, err } } if options.SignatureKeyPath != "" { if err := copy.File(options.SignatureKeyPath, filepath.Join(options.ScratchDir, "loader/keys/auto", constants.SignatureKeyAsset)); err != nil { - return err + return nil, err } } @@ -176,42 +183,30 @@ func CreateUEFI(printf func(string, ...any), options UEFIOptions) error { filepath.Join(options.ScratchDir, "loader"), "::", ); err != nil { - return err + return nil, err } // fixup directory timestamps recursively if err := utils.TouchFiles(printf, options.ScratchDir); err != nil { - return err + return nil, err } printf("creating ISO image") - // ref: https://askubuntu.com/questions/1110651/how-to-produce-an-iso-image-that-boots-only-on-uefi/1111760#1111760 - args := []string{ - "-e", "--interval:appended_partition_2:all::", // use appended partition 2 for EFI - "-append_partition", "2", "0xef", efiBootImg, - "-partition_cyl_align", // pad partition to cylinder boundary - "all", - "-partition_offset", "16", // support booting from USB - "-iso_mbr_part_type", "0x83", // just to have more clear info when doing a fdisk -l - "-no-emul-boot", - "-m", "efiboot.img", // exclude the EFI boot image from the ISO - "-o", options.OutPath, - options.ScratchDir, - } - - if quirks.New(options.Version).SupportsISOLabel() { - label := Label(options.Version, true) - - args = append(args, - "-volid", VolumeID(label), - "-volset", label, - ) - } - - if _, err := cmd.Run("xorrisofs", args...); err != nil { - return err - } - - return nil + return &ExecutorOptions{ + Command: "xorrisofs", + Version: options.Version, + Arguments: []string{ + "-e", "--interval:appended_partition_2:all::", // use appended partition 2 for EFI + "-append_partition", "2", "0xef", efiBootImg, + "-partition_cyl_align", // pad partition to cylinder boundary + "all", + "-partition_offset", "16", // support booting from USB + "-iso_mbr_part_type", "0x83", // just to have more clear info when doing a fdisk -l + "-no-emul-boot", + "-m", "efiboot.img", // exclude the EFI boot image from the ISO + "-o", options.OutPath, + options.ScratchDir, + }, + }, nil } diff --git a/pkg/imager/out.go b/pkg/imager/out.go index 92485feb4a8..9da778caecf 100644 --- a/pkg/imager/out.go +++ b/pkg/imager/out.go @@ -83,7 +83,7 @@ func (i *Imager) outCmdline(path string) error { return os.WriteFile(path, []byte(i.cmdline), 0o644) } -//nolint:gocyclo +//nolint:gocyclo,cyclop func (i *Imager) outISO(ctx context.Context, path string, report *reporter.Reporter) error { printf := progressPrintf(report, reporter.Update{Message: "building ISO...", Status: reporter.StatusRunning}) @@ -104,7 +104,10 @@ func (i *Imager) outISO(ctx context.Context, path string, report *reporter.Repor } } - if i.prof.SecureBootEnabled() { + var generator iso.Generator + + switch { + case i.prof.SecureBootEnabled(): isoOptions := pointer.SafeDeref(i.prof.Output.ISOOptions) var signer pesign.CertificateSigner @@ -120,7 +123,7 @@ func (i *Imager) outISO(ctx context.Context, path string, report *reporter.Repor return fmt.Errorf("failed to write uki.der: %w", err) } - options := iso.UEFIOptions{ + options := iso.Options{ UKIPath: i.ukiPath, SDBootPath: i.sdBootPath, @@ -179,20 +182,52 @@ func (i *Imager) outISO(ctx context.Context, path string, report *reporter.Repor options.SignatureKeyPath = i.prof.Input.SecureBoot.SignatureKeyPath } - err = iso.CreateUEFI(printf, options) - } else { - err = iso.CreateGRUB(printf, iso.GRUBOptions{ + generator, err = options.CreateUEFI(printf) + if err != nil { + return err + } + case quirks.New(i.prof.Version).UseSDBootForUEFI(): + options := iso.Options{ KernelPath: i.prof.Input.Kernel.Path, InitramfsPath: i.initramfsPath, Cmdline: i.cmdline, - Version: i.prof.Version, + + UKIPath: i.ukiPath, + SDBootPath: i.sdBootPath, + + SDBootSecureBootEnrollKeys: "off", + + Arch: i.prof.Arch, + Version: i.prof.Version, ScratchDir: scratchSpace, OutPath: path, - }) + } + + generator, err = options.CreateHybrid(printf) + if err != nil { + return err + } + default: + options := iso.Options{ + KernelPath: i.prof.Input.Kernel.Path, + InitramfsPath: i.initramfsPath, + Cmdline: i.cmdline, + + Arch: i.prof.Arch, + Version: i.prof.Version, + + ScratchDir: scratchSpace, + OutPath: path, + } + + generator, err = options.CreateGRUB(printf) + if err != nil { + return err + } } - if err != nil { + if err := generator.Generate(); err != nil { return err }