diff --git a/toolkit/tools/internal/initrdutils/initrdread.go b/toolkit/tools/internal/initrdutils/initrdread.go new file mode 100644 index 0000000000..a1a56e1458 --- /dev/null +++ b/toolkit/tools/internal/initrdutils/initrdread.go @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package initrdutils + +import ( + "bufio" + "bytes" + "fmt" + "io" + "io/fs" + "os" + + "github.com/cavaliergopher/cpio" + "github.com/klauspost/compress/zstd" + "github.com/klauspost/pgzip" +) + +// Magic byte signatures for the compression formats we recognize at the start +// of an initramfs stream. +// +// - gzip: RFC 1952 section 2.3.1 ("ID1 ID2" = 1F 8B) +// https://www.rfc-editor.org/rfc/rfc1952#section-2.3.1 +// - zstd: RFC 8478 section 3.1.1 ("Magic_Number" = 0xFD2FB528 little-endian) +// https://www.rfc-editor.org/rfc/rfc8478#section-3.1.1 +var ( + magicGzip = []byte{0x1f, 0x8b} + magicZstd = []byte{0x28, 0xb5, 0x2f, 0xfd} + longestMagicSize = len(magicZstd) +) + +// ReadFirstFileFromInitrd scans an initramfs cpio archive once and returns the content of the first candidate path in +// the provided list that exists as a regular file. Returns an error wrapping fs.ErrNotExist if none of the candidates +// is present. +func ReadFirstFileFromInitrd(initrdPath string, candidates []string) (content []byte, foundPath string, err error) { + f, err := os.Open(initrdPath) + if err != nil { + return nil, "", fmt.Errorf("failed to open initrd (%s):\n%w", initrdPath, err) + } + defer f.Close() + + decompressed, err := openInitrdDecompressor(f) + if err != nil { + return nil, "", fmt.Errorf("failed to decompress initrd (%s):\n%w", initrdPath, err) + } + defer decompressed.Close() + + wanted := make(map[string]struct{}, len(candidates)) + for _, c := range candidates { + wanted[c] = struct{}{} + } + + found := make(map[string][]byte, len(candidates)) + cpioReader := cpio.NewReader(decompressed) + for { + hdr, err := cpioReader.Next() + if err == io.EOF { + break + } + if err != nil { + return nil, "", fmt.Errorf("failed to read cpio header from initrd (%s):\n%w", initrdPath, err) + } + + if _, ok := wanted[hdr.Name]; !ok { + continue + } + + // Only read regular files. + if hdr.Mode&cpio.ModeType != cpio.TypeReg { + continue + } + + data, err := io.ReadAll(cpioReader) + if err != nil { + return nil, "", fmt.Errorf("failed to read (%s) from initrd (%s):\n%w", hdr.Name, initrdPath, err) + } + + found[hdr.Name] = data + } + + for _, candidate := range candidates { + if data, ok := found[candidate]; ok { + return data, candidate, nil + } + } + + return nil, "", fmt.Errorf("failed to find any of %v in initrd (%s): %w", candidates, initrdPath, fs.ErrNotExist) +} + +// openInitrdDecompressor auto-detects the compression format of an initramfs stream from its leading magic bytes and +// returns a reader over the decompressed (cpio) content. +func openInitrdDecompressor(r io.Reader) (io.ReadCloser, error) { + br := bufio.NewReader(r) + head, err := br.Peek(longestMagicSize) + if err != nil && err != io.EOF { + return nil, fmt.Errorf("failed to peek initrd magic bytes:\n%w", err) + } + + switch { + case bytes.HasPrefix(head, magicGzip): + gz, err := pgzip.NewReader(br) + if err != nil { + return nil, fmt.Errorf("failed to open gzip reader for initrd:\n%w", err) + } + return gz, nil + + case bytes.HasPrefix(head, magicZstd): + zr, err := zstd.NewReader(br) + if err != nil { + return nil, fmt.Errorf("failed to open zstd reader for initrd:\n%w", err) + } + + // zstd.Decoder satisfies io.Reader but not io.Closer since its Close() returns no error, so wrap it to + // implement io.ReadCloser, like pgzip.Reader. + return zstdReadCloser{Decoder: zr}, nil + + default: + return nil, fmt.Errorf("unrecognized initrd compression format (leading bytes: % x)", head) + } +} diff --git a/toolkit/tools/internal/initrdutils/zstdreadcloser.go b/toolkit/tools/internal/initrdutils/zstdreadcloser.go new file mode 100644 index 0000000000..164639a094 --- /dev/null +++ b/toolkit/tools/internal/initrdutils/zstdreadcloser.go @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package initrdutils + +import ( + "github.com/klauspost/compress/zstd" +) + +// zstdReadCloser adapts *zstd.Decoder (whose Close() returns no error) to io.ReadCloser. +type zstdReadCloser struct{ *zstd.Decoder } + +func (z zstdReadCloser) Close() error { + z.Decoder.Close() + return nil +} diff --git a/toolkit/tools/internal/targetos/targetos.go b/toolkit/tools/internal/targetos/targetos.go index 164e5d4c4e..617483bf0d 100644 --- a/toolkit/tools/internal/targetos/targetos.go +++ b/toolkit/tools/internal/targetos/targetos.go @@ -10,6 +10,7 @@ import ( "path/filepath" "github.com/microsoft/azure-linux-image-tools/toolkit/tools/internal/envfile" + "github.com/microsoft/azure-linux-image-tools/toolkit/tools/internal/initrdutils" "github.com/microsoft/azure-linux-image-tools/toolkit/tools/internal/version" ) @@ -74,10 +75,27 @@ var ( Version: []int{24, 4}, } + // OsReleaseCandidates lists the on-rootfs paths to probe for the os-release(5) file, in preference order. OsReleaseFileCandidates = []string{ "/etc/os-release", "/usr/lib/os-release", } + + // initrdReleaseCandidates lists the in-initrd paths to probe for an os-release(5)-type file, in preference order. + // These are matched against cpio member names, which are stored relative to the archive root, so they must not have + // a leading slash. + // + // os-release is the canonical filename and is the only candidate present in full-OS initrds built by Image + // Customizer (which pack the source rootfs directly, in which initrd-release does not exist at all). + // + // initrd-release is the dracut-runtime variant emitted by dracut's 99base module. os-release symlinks to this file + // in such cases, so it is a necessary fallback. + initrdReleaseCandidates = []string{ + "etc/os-release", + "usr/lib/os-release", + "etc/initrd-release", + "usr/lib/initrd-release", + } ) func New(distroId Distro, versionId string) TargetOs { @@ -90,6 +108,9 @@ func New(distroId Distro, versionId string) TargetOs { } } +// GetInstalledTargetOs reads the os-release(5) file from the given rootfs and resolves it to a TargetOs. +// +// Returns an error wrapping fs.ErrNotExist when none of the candidates exist on the rootfs. func GetInstalledTargetOs(rootfs string) (TargetOs, error) { var err error var fields map[string]string @@ -113,11 +134,34 @@ func GetInstalledTargetOs(rootfs string) (TargetOs, error) { } targetOs, err := GetInstalledTargetOsFromEnvFields(fields) + if err != nil { + return TargetOs{}, fmt.Errorf("failed to determine target OS from os-release file:\n%w", err) + } + + return targetOs, nil +} + +// GetInitrdTargetOs reads an os-release(5)-type file (os-release or dracut's initrd-release) from the given initrd and +// resolves it to a TargetOs. +// +// Returns an error wrapping fs.ErrNotExist when none of the candidates exist in the initrd. +func GetInitrdTargetOs(initrdPath string) (TargetOs, error) { + content, foundPath, err := initrdutils.ReadFirstFileFromInitrd(initrdPath, initrdReleaseCandidates) if err != nil { return TargetOs{}, err } - return targetOs, err + fields, err := envfile.ParseEnv(string(content)) + if err != nil { + return TargetOs{}, fmt.Errorf("failed to read (%s) file from initrd (%s):\n%w", foundPath, initrdPath, err) + } + + targetOs, err := GetInstalledTargetOsFromEnvFields(fields) + if err != nil { + return TargetOs{}, fmt.Errorf("failed to determine target OS from initrd-release file:\n%w", err) + } + + return targetOs, nil } func GetInstalledTargetOsFromEnvFields(fields map[string]string) (TargetOs, error) { @@ -171,7 +215,7 @@ func GetInstalledTargetOsFromEnvFields(fields map[string]string) (TargetOs, erro }, nil default: - return TargetOs{}, fmt.Errorf("unknown ID (%s) in os-release", distroId) + return TargetOs{}, fmt.Errorf("unknown ID (%s)", distroId) } } diff --git a/toolkit/tools/pkg/imagecustomizerlib/artifactsinputoutput.go b/toolkit/tools/pkg/imagecustomizerlib/artifactsinputoutput.go index 4d09a486c5..447fe86522 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/artifactsinputoutput.go +++ b/toolkit/tools/pkg/imagecustomizerlib/artifactsinputoutput.go @@ -96,7 +96,7 @@ func outputArtifacts(ctx context.Context, items []imagecustomizerapi.OutputArtif defer systemBootPartitionMount.Close() // Detect system architecture - _, bootConfig, err := getBootArchConfig() + bootConfig, err := distroHandler.GetBootArchConfig() if err != nil { return err } diff --git a/toolkit/tools/pkg/imagecustomizerlib/artifactsinputoutput_test.go b/toolkit/tools/pkg/imagecustomizerlib/artifactsinputoutput_test.go index 1c677d7f44..2dd4eae139 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/artifactsinputoutput_test.go +++ b/toolkit/tools/pkg/imagecustomizerlib/artifactsinputoutput_test.go @@ -55,7 +55,7 @@ func TestOutputAndInjectArtifacts(t *testing.T) { return } - espFiles := verifyAndSignOutputtedArtifacts(t, outputArtifactsDir, false) + espFiles := verifyAndSignOutputtedArtifacts(t, baseImageInfo, outputArtifactsDir, false) // Use new buildDir to ensure the buildDir is created if it doesn't exist. buildDirInject := filepath.Join(buildDir, "inject") @@ -163,7 +163,7 @@ func TestOutputAndInjectArtifactsCosi(t *testing.T) { return } - espFiles := verifyAndSignOutputtedArtifacts(t, outputArtifactsDir, true) + espFiles := verifyAndSignOutputtedArtifacts(t, baseImageInfo, outputArtifactsDir, true) // Inject artifacts into image. options := InjectFilesOptions{ @@ -305,7 +305,19 @@ func TestOutputAndInjectArtifactsCosi(t *testing.T) { "root", buildDir, "", "restart-on-corruption", false /*inlineVerity*/) } -func verifyAndSignOutputtedArtifacts(t *testing.T, outputArtifactsDir string, expectVerityHash bool) []string { +func verifyAndSignOutputtedArtifacts(t *testing.T, baseImageInfo testBaseImageInfo, outputArtifactsDir string, expectVerityHash bool) []string { + // Resolve the distro-version's boot file layout so artifact destinations can be checked against the distro's + // actual ESP paths. The boot file names differ per distro-version (for example, Azure Linux 4 uses the Fedora-style + // uppercase BOOTX64.EFI shim, while Azure Linux 3 uses lowercase bootx64.efi). + distroHandler, err := NewDistroHandler(baseImageInfo.TargetOs()) + if !assert.NoError(t, err) { + return nil + } + bootConfig, err := distroHandler.GetBootArchConfig() + if !assert.NoError(t, err) { + return nil + } + // Confirm inject-files.yaml was generated injectConfigPath := filepath.Join(outputArtifactsDir, "inject-files.yaml") exists, err := file.PathExists(injectConfigPath) @@ -333,15 +345,15 @@ func verifyAndSignOutputtedArtifacts(t *testing.T, outputArtifactsDir string, ex switch entry.Type { case imagecustomizerapi.OutputArtifactsItemShim: - assert.True(t, strings.HasPrefix(entry.Destination, "/EFI/BOOT/boot"), "Expected shim destination to start with /EFI/BOOT/boot") - assert.True(t, strings.HasSuffix(entry.Destination, ".efi"), "Expected shim destination to end with .efi") + expectedShimDestination := filepath.Join("/", bootConfig.espBootBinaryPath) + assert.Equal(t, expectedShimDestination, entry.Destination, "Expected shim destination to match the distro ESP boot binary path") assert.True(t, strings.HasPrefix(entry.Source, "./shim/"), "Expected shim source to be in shim/ subdirectory") hasShim = true espFiles = append(espFiles, entry.Destination) case imagecustomizerapi.OutputArtifactsItemBootloader: - assert.True(t, strings.HasPrefix(entry.Destination, "/EFI/BOOT/grub"), "Expected bootloader destination to start with /EFI/BOOT/grub") - assert.True(t, strings.HasSuffix(entry.Destination, ".efi"), "Expected bootloader destination to end with .efi") + expectedBootloaderDestination := filepath.Join("/", bootConfig.espGrubBinaryPath) + assert.Equal(t, expectedBootloaderDestination, entry.Destination, "Expected bootloader destination to match the distro ESP grub binary path") assert.True(t, strings.HasPrefix(entry.Source, "./bootloader/"), "Expected bootloader source to be in bootloader/ subdirectory") hasBootloader = true espFiles = append(espFiles, entry.Destination) diff --git a/toolkit/tools/pkg/imagecustomizerlib/customizeuki.go b/toolkit/tools/pkg/imagecustomizerlib/customizeuki.go index e791135927..39f0601d7e 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/customizeuki.go +++ b/toolkit/tools/pkg/imagecustomizerlib/customizeuki.go @@ -258,7 +258,7 @@ func prepareUkiHelper(ctx context.Context, buildDir string, uki *imagecustomizer if uki.Mode == imagecustomizerapi.UkiModeModify { logger.Log.Infof("UKI mode is 'modify', skipping UKI preparation (will modify addon only)") - _, bootConfig, err := getBootArchConfig() + bootConfig, err := distroHandler.GetBootArchConfig() if err != nil { return err } @@ -295,7 +295,7 @@ func prepareUkiHelper(ctx context.Context, buildDir string, uki *imagecustomizer } // Detect system architecture. - _, bootConfig, err := getBootArchConfig() + bootConfig, err := distroHandler.GetBootArchConfig() if err != nil { return err } @@ -503,13 +503,13 @@ func findKernelsAndInitramfs(bootDir string) (map[string]string, error) { } // createUkiInModifyMode modifies UKI addons without touching the main UKI files. -func createUkiInModifyMode(ctx context.Context, rc *ResolvedConfig) error { +func createUkiInModifyMode(ctx context.Context, rc *ResolvedConfig, distroHandler DistroHandler) error { _, span := otel.GetTracerProvider().Tracer(OtelTracerName).Start(ctx, "customize_uki_modify_mode") defer span.End() var err error - _, bootConfig, err := getBootArchConfig() + bootConfig, err := distroHandler.GetBootArchConfig() if err != nil { return err } @@ -618,7 +618,7 @@ func modifyUkiAddon(ukiFilePath string, stubPath string, rc *ResolvedConfig) err return nil } -func createUki(ctx context.Context, rc *ResolvedConfig) error { +func createUki(ctx context.Context, rc *ResolvedConfig, distroHandler DistroHandler) error { logger.Log.Infof("Creating UKIs") // If mode is 'passthrough', skip UKI creation to preserve existing UKIs @@ -630,7 +630,7 @@ func createUki(ctx context.Context, rc *ResolvedConfig) error { // If mode is 'modify', only modify UKI addons (preserve main UKI) if rc.Uki != nil && rc.Uki.Mode == imagecustomizerapi.UkiModeModify { logger.Log.Infof("UKI mode is 'modify', modifying UKI addons only") - err := createUkiInModifyMode(ctx, rc) + err := createUkiInModifyMode(ctx, rc, distroHandler) if err != nil { return fmt.Errorf("failed to modify UKI addons in modify mode:\n%w", err) } @@ -642,7 +642,7 @@ func createUki(ctx context.Context, rc *ResolvedConfig) error { var err error - _, bootConfig, err := getBootArchConfig() + bootConfig, err := distroHandler.GetBootArchConfig() if err != nil { return err } diff --git a/toolkit/tools/pkg/imagecustomizerlib/distrohandler.go b/toolkit/tools/pkg/imagecustomizerlib/distrohandler.go index ab304bcd07..41ae313757 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/distrohandler.go +++ b/toolkit/tools/pkg/imagecustomizerlib/distrohandler.go @@ -133,6 +133,10 @@ type DistroHandler interface { // Distro has a root partition that is missing placeholder directories for special mounts like /dev. RootMissingMountDirectories() bool + + // GetBootArchConfig returns the boot-files configuration appropriate for this distro on the current runtime + // architecture. + GetBootArchConfig() (BootFilesArchConfig, error) } // NewDistroHandler creates a distro handler directly from TargetOs @@ -185,3 +189,20 @@ func handleUnsupportedDistroVersion(rc *ResolvedConfig, targetOs targetos.Target return nil } + +// NewDistroHandlerFromInitrd creates a distro handler by detecting the OS from the dracut-emitted initrd-release file +// inside the initramfs at initrdPath. Used in ISO-to-ISO pipelines where no rootfs is mounted yet but a canonical +// distro identity is needed to pick the correct boot-file layout. +func NewDistroHandlerFromInitrd(initrdPath string) (DistroHandler, error) { + targetOs, err := targetos.GetInitrdTargetOs(initrdPath) + if err != nil { + return nil, fmt.Errorf("failed to determine the target OS from initrd (%s):\n%w", initrdPath, err) + } + + distroHandler, err := NewDistroHandler(targetOs) + if err != nil { + return nil, err + } + + return distroHandler, nil +} diff --git a/toolkit/tools/pkg/imagecustomizerlib/distrohandler_acl.go b/toolkit/tools/pkg/imagecustomizerlib/distrohandler_acl.go index 818f1f9d3c..6c055f9128 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/distrohandler_acl.go +++ b/toolkit/tools/pkg/imagecustomizerlib/distrohandler_acl.go @@ -287,3 +287,7 @@ func (d *aclDistroHandler) GrubEfiPackage() string { func (d *aclDistroHandler) RootMissingMountDirectories() bool { return true } + +func (d *aclDistroHandler) GetBootArchConfig() (BootFilesArchConfig, error) { + return bootArchConfigFromMap(bootloaderFilesConfigAzureLinux) +} diff --git a/toolkit/tools/pkg/imagecustomizerlib/distrohandler_azurelinux.go b/toolkit/tools/pkg/imagecustomizerlib/distrohandler_azurelinux.go index e8acb20244..4125c7f0bf 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/distrohandler_azurelinux.go +++ b/toolkit/tools/pkg/imagecustomizerlib/distrohandler_azurelinux.go @@ -224,3 +224,7 @@ func (d *azureLinuxDistroHandler) GrubEfiPackage() string { func (d *azureLinuxDistroHandler) RootMissingMountDirectories() bool { return false } + +func (d *azureLinuxDistroHandler) GetBootArchConfig() (BootFilesArchConfig, error) { + return bootArchConfigFromMap(bootloaderFilesConfigAzureLinux) +} diff --git a/toolkit/tools/pkg/imagecustomizerlib/distrohandler_azurelinux4.go b/toolkit/tools/pkg/imagecustomizerlib/distrohandler_azurelinux4.go index 9c0fe32eb3..581068404b 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/distrohandler_azurelinux4.go +++ b/toolkit/tools/pkg/imagecustomizerlib/distrohandler_azurelinux4.go @@ -274,3 +274,7 @@ func (d *azureLinux4DistroHandler) GrubEfiPackage() string { func (d *azureLinux4DistroHandler) RootMissingMountDirectories() bool { return false } + +func (d *azureLinux4DistroHandler) GetBootArchConfig() (BootFilesArchConfig, error) { + return bootArchConfigFromMap(bootloaderFilesConfigFedora) +} diff --git a/toolkit/tools/pkg/imagecustomizerlib/distrohandler_fedora.go b/toolkit/tools/pkg/imagecustomizerlib/distrohandler_fedora.go index e7819f463f..5b168fff3e 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/distrohandler_fedora.go +++ b/toolkit/tools/pkg/imagecustomizerlib/distrohandler_fedora.go @@ -29,14 +29,58 @@ type fedoraDistroHandler struct { } const ( - grubEfiPackageFedoraAmd64 = "grub2-efi-x64" - grubEfiPackageFedoraArm64 = "grub2-efi-aa64" - shimPackageFedoraAmd64 = "shim-x64" - shimPackageFedoraArm64 = "shim-aa64" + grubEfiPackageFedoraAmd64 = "grub2-efi-x64" + grubEfiPackageFedoraArm64 = "grub2-efi-aa64" + shimPackageFedoraAmd64 = "shim-x64" + shimPackageFedoraArm64 = "shim-aa64" + + isoBootloaderDirFedora = "/EFI/BOOT" + bootx64BinaryFedora = "BOOTX64.EFI" + bootAA64BinaryFedora = "BOOTAA64.EFI" + grubToolsPackageFedora = "grub2-tools" grubPcModulesPackageFedora = "grub2-pc-modules" ) +// bootloaderFilesConfigFedora is the boot-files map for Fedora-style ESPs (Azure Linux 4 and Fedora). +// +// Fedora-style distros do not ship a grub-noprefix binary, so grubNoPrefixBinary and +// osEspGrubNoPrefixBinaryPath are left empty. +var bootloaderFilesConfigFedora = map[string]BootFilesArchConfig{ + "amd64": { + bootBinary: bootx64BinaryFedora, + grubBinary: grubx64Binary, + grubNoPrefixBinary: "", + espBootBinaryPath: espBootloaderDir + "/" + bootx64BinaryFedora, + espGrubBinaryPath: espBootloaderDir + "/" + grubx64Binary, + osEspBootBinaryPath: osEspBootloaderDir + "/" + bootx64BinaryFedora, + osEspGrubBinaryPath: osEspBootloaderDir + "/" + grubx64Binary, + osEspGrubNoPrefixBinaryPath: "", + isoBootBinaryPath: isoBootloaderDirFedora + "/" + bootx64BinaryFedora, + isoGrubBinaryPath: isoBootloaderDirFedora + "/" + grubx64Binary, + ukiEfiStubBinary: ukiEfiStubx64Binary, + ukiEfiStubBinaryPath: ukiEfiStubDir + "/" + ukiEfiStubx64Binary, + ukiAddonStubBinary: ukiAddonStubx64Binary, + ukiAddonStubBinaryPath: ukiEfiStubDir + "/" + ukiAddonStubx64Binary, + }, + "arm64": { + bootBinary: bootAA64BinaryFedora, + grubBinary: grubAA64Binary, + grubNoPrefixBinary: "", + espBootBinaryPath: espBootloaderDir + "/" + bootAA64BinaryFedora, + espGrubBinaryPath: espBootloaderDir + "/" + grubAA64Binary, + osEspBootBinaryPath: osEspBootloaderDir + "/" + bootAA64BinaryFedora, + osEspGrubBinaryPath: osEspBootloaderDir + "/" + grubAA64Binary, + osEspGrubNoPrefixBinaryPath: "", + isoBootBinaryPath: isoBootloaderDirFedora + "/" + bootAA64BinaryFedora, + isoGrubBinaryPath: isoBootloaderDirFedora + "/" + grubAA64Binary, + ukiEfiStubBinary: ukiEfiStubAA64Binary, + ukiEfiStubBinaryPath: ukiEfiStubDir + "/" + ukiEfiStubAA64Binary, + ukiAddonStubBinary: ukiAddonStubAA64Binary, + ukiAddonStubBinaryPath: ukiEfiStubDir + "/" + ukiAddonStubAA64Binary, + }, +} + func newFedoraDistroHandler(targetOs targetos.TargetOs) *fedoraDistroHandler { logger.Log.Debugf("Distro handler: Fedora (distro='%s', versionid='%s')", targetOs.Distro, targetOs.VersionId) @@ -236,3 +280,7 @@ func (d *fedoraDistroHandler) GrubEfiPackage() string { func (d *fedoraDistroHandler) RootMissingMountDirectories() bool { return false } + +func (d *fedoraDistroHandler) GetBootArchConfig() (BootFilesArchConfig, error) { + return bootArchConfigFromMap(bootloaderFilesConfigFedora) +} diff --git a/toolkit/tools/pkg/imagecustomizerlib/distrohandler_ubuntu.go b/toolkit/tools/pkg/imagecustomizerlib/distrohandler_ubuntu.go index 70d72155d0..bf271b3b3b 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/distrohandler_ubuntu.go +++ b/toolkit/tools/pkg/imagecustomizerlib/distrohandler_ubuntu.go @@ -242,3 +242,7 @@ func (d *ubuntuDistroHandler) GrubEfiPackage() string { func (d *ubuntuDistroHandler) RootMissingMountDirectories() bool { return false } + +func (d *ubuntuDistroHandler) GetBootArchConfig() (BootFilesArchConfig, error) { + return bootArchConfigFromMap(bootloaderFilesConfigAzureLinux) +} diff --git a/toolkit/tools/pkg/imagecustomizerlib/imagecustomizer.go b/toolkit/tools/pkg/imagecustomizerlib/imagecustomizer.go index 6552ddf023..0ed83d233f 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/imagecustomizer.go +++ b/toolkit/tools/pkg/imagecustomizerlib/imagecustomizer.go @@ -498,7 +498,7 @@ func customizeOSContents(ctx context.Context, rc *ResolvedConfig) (imageMetadata } if rc.Uki != nil { - err = createUki(ctx, rc) + err = createUki(ctx, rc, im.distroHandler) if err != nil { return im, fmt.Errorf("%w:\n%w", ErrCustomizeCreateUkis, err) } diff --git a/toolkit/tools/pkg/imagecustomizerlib/imageutils.go b/toolkit/tools/pkg/imagecustomizerlib/imageutils.go index 5137d2c41f..a6709997cb 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/imageutils.go +++ b/toolkit/tools/pkg/imagecustomizerlib/imageutils.go @@ -545,7 +545,7 @@ func getInstalledTargetOsFromPartitionLayout(diskPartitions []diskutils.Partitio targetOs, err := targetos.GetInstalledTargetOsFromEnvFields(fields) if err != nil { - return targetos.TargetOs{}, err + return targetos.TargetOs{}, fmt.Errorf("failed to determine target OS from os-release file:\n%w", err) } return targetOs, nil diff --git a/toolkit/tools/pkg/imagecustomizerlib/liveosisoartifactstore.go b/toolkit/tools/pkg/imagecustomizerlib/liveosisoartifactstore.go index abe2fcc302..912cf3dbdb 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/liveosisoartifactstore.go +++ b/toolkit/tools/pkg/imagecustomizerlib/liveosisoartifactstore.go @@ -81,11 +81,17 @@ func (b *IsoArtifactsStore) cleanUp() error { return nil } -func containsGrubNoPrefix(filePaths []string) (bool, error) { - _, bootFilesConfig, err := getBootArchConfig() +func containsGrubNoPrefix(filePaths []string, distroHandler DistroHandler) (bool, error) { + bootFilesConfig, err := distroHandler.GetBootArchConfig() if err != nil { return false, err } + + // Distros without a grub-noprefix binary leave grubNoPrefixBinary empty. + if bootFilesConfig.grubNoPrefixBinary == "" { + return false, nil + } + for _, filePath := range filePaths { if filepath.Base(filePath) == bootFilesConfig.grubNoPrefixBinary { return true, nil @@ -218,7 +224,7 @@ func createIsoFilesStoreFromMountedImage(inputArtifactsStore *IsoArtifactsStore, return nil, fmt.Errorf("failed to scan /boot folder:\n%w", err) } - usingGrubNoPrefix, err := containsGrubNoPrefix(bootFolderFilePaths) + usingGrubNoPrefix, err := containsGrubNoPrefix(bootFolderFilePaths, distroHandler) if err != nil { return nil, err } @@ -252,7 +258,7 @@ func createIsoFilesStoreFromMountedImage(inputArtifactsStore *IsoArtifactsStore, // in them (i.e. user files that we do not manipulate - but copy as-is). scheduleAdditionalFile := true - _, bootFilesConfig, err := getBootArchConfig() + bootFilesConfig, err := distroHandler.GetBootArchConfig() if err != nil { return nil, err } @@ -260,16 +266,17 @@ func createIsoFilesStoreFromMountedImage(inputArtifactsStore *IsoArtifactsStore, osEspGrubBinaryPath := bootFilesConfig.osEspGrubBinaryPath osEspGrubNoPrefixBinaryPath := bootFilesConfig.osEspGrubNoPrefixBinaryPath - switch relativeFilePath { - case osEspBootBinaryPath: + switch { + case relativeFilePath == osEspBootBinaryPath: filesStore.bootEfiPath = targetPath scheduleAdditionalFile = false // No additional file scheduling - case osEspGrubBinaryPath, osEspGrubNoPrefixBinaryPath: + case relativeFilePath == osEspGrubBinaryPath, + osEspGrubNoPrefixBinaryPath != "" && relativeFilePath == osEspGrubNoPrefixBinaryPath: filesStore.grubEfiPath = targetPath scheduleAdditionalFile = false // No additional file scheduling - case isoGrubCfgPath: + case relativeFilePath == isoGrubCfgPath: if usingGrubNoPrefix { // When using the grubx64-noprefix.efi, the 'prefix' grub // variable is set to an empty string. When 'prefix' is an @@ -277,7 +284,7 @@ func createIsoFilesStoreFromMountedImage(inputArtifactsStore *IsoArtifactsStore, // media, the bootloader defaults to looking for grub.cfg at // /EFI/BOOT/grub.cfg. // So, below, we ensure that grub.cfg file will be placed where - // grubx64-nopreifx.efi will be looking for it. + // grubx64-noprefix.efi will be looking for it. // // Note that this grub.cfg is the only file that needs to be // copied to that EFI/BOOT location. The rest of the files (like @@ -292,7 +299,7 @@ func createIsoFilesStoreFromMountedImage(inputArtifactsStore *IsoArtifactsStore, // We will place the pxe grub config next to the iso grub config. filesStore.pxeGrubCfgPath = filepath.Join(filepath.Dir(filesStore.isoGrubCfgPath), pxeGrubCfg) scheduleAdditionalFile = false - case isoInitrdPath: + case relativeFilePath == isoInitrdPath: filesStore.initrdImagePath = targetPath scheduleAdditionalFile = false default: @@ -367,7 +374,7 @@ func createIsoFilesStoreFromMountedImage(inputArtifactsStore *IsoArtifactsStore, } } - _, bootFilesConfig, err := getBootArchConfig() + bootFilesConfig, err := distroHandler.GetBootArchConfig() if err != nil { return nil, err } @@ -378,9 +385,13 @@ func createIsoFilesStoreFromMountedImage(inputArtifactsStore *IsoArtifactsStore, } if filesStore.grubEfiPath == "" { - return nil, fmt.Errorf("failed to find the grub efi file (%s or %s):\n"+ + grubBinaryNames := bootFilesConfig.grubBinary + if bootFilesConfig.grubNoPrefixBinary != "" { + grubBinaryNames = fmt.Sprintf("%s or %s", bootFilesConfig.grubBinary, bootFilesConfig.grubNoPrefixBinary) + } + return nil, fmt.Errorf("failed to find the grub efi file (%s):\n"+ "this file is provided by the %s package", - bootFilesConfig.grubBinary, bootFilesConfig.grubNoPrefixBinary, + grubBinaryNames, distroHandler.GrubEfiPackage()) } @@ -448,7 +459,49 @@ func createIsoFilesStoreFromIsoImage(isoImageFile, storeDir string) (filesStore filesStore.kernelBootFiles = make(map[string]*KernelBootFiles) filesStore.kdumpBootFiles = make(map[string]*KdumpBootFiles) - _, bootFilesConfig, err := getBootArchConfig() + // Resolve the distro from the ISO's initrd. Which initrd to read depends on the ISO's initramfsType. + squashfsPath := filepath.Join(artifactsDir, liveOSImagePath) + squashfsExists, err := file.PathExists(squashfsPath) + if err != nil { + return nil, fmt.Errorf("failed to check for squashfs at (%s):\n%w", squashfsPath, err) + } + + // A bootstrap ISO carries a squashfs root plus per-kernel dracut initrds, while a full-OS ISO has no squashfs and + // instead packs the rootfs into a single initrd. Use the squashfs presence to pick which initrd to read for distro + // detection. + var initrdPath string + if squashfsExists { + filesStore.squashfsImagePath = squashfsPath + + // Any of the per-kernel dracut initrds is fine for distro detection since they're all from the same OS. + for _, f := range isoFiles { + base := filepath.Base(f) + isInitrd := strings.HasPrefix(base, initramfsPrefix) || strings.HasPrefix(base, initrdPrefix) + if isInitrd && strings.HasSuffix(base, ".img") { + initrdPath = f + break + } + } + if initrdPath == "" { + return nil, fmt.Errorf("bootstrap ISO has no per-kernel initrd") + } + } else { + initrdPath = filepath.Join(artifactsDir, isoInitrdPath) + initrdExists, err := file.PathExists(initrdPath) + if err != nil { + return nil, fmt.Errorf("failed to check for initrd at (%s):\n%w", initrdPath, err) + } + if !initrdExists { + return nil, fmt.Errorf("full OS ISO has no initrd at expected path (%s)", initrdPath) + } + } + + distroHandler, err := NewDistroHandlerFromInitrd(initrdPath) + if err != nil { + return nil, fmt.Errorf("failed to detect distro from initrd (%s):\n%w", initrdPath, err) + } + + bootFilesConfig, err := distroHandler.GetBootArchConfig() if err != nil { return nil, err } diff --git a/toolkit/tools/pkg/imagecustomizerlib/liveosisobuilder.go b/toolkit/tools/pkg/imagecustomizerlib/liveosisobuilder.go index beef461cc1..8afe64f30e 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/liveosisobuilder.go +++ b/toolkit/tools/pkg/imagecustomizerlib/liveosisobuilder.go @@ -341,7 +341,7 @@ func createLiveOSFromRawHelper(ctx context.Context, buildDir string, inputArtifa case imagecustomizerapi.ImageFormatTypePxeDir, imagecustomizerapi.ImageFormatTypePxeTar: err = createPXEArtifacts(isoBuildDir, outputFormat, liveosConfig.initramfsType, artifactsStore, liveosConfig.kdumpBootFiles, liveosConfig.additionalFiles, - liveosConfig.bootstrapBaseUrl, liveosConfig.bootstrapFileUrl, outputPath) + liveosConfig.bootstrapBaseUrl, liveosConfig.bootstrapFileUrl, outputPath, distroHandler) if err != nil { return fmt.Errorf("failed to generate PXE artifacts\n%w", err) } @@ -392,7 +392,7 @@ func repackageLiveOSHelper(isoBuildDir string, liveosConfig LiveOSConfig, inputA case imagecustomizerapi.ImageFormatTypePxeDir, imagecustomizerapi.ImageFormatTypePxeTar: err = createPXEArtifacts(isoBuildDir, outputFormat, liveosConfig.initramfsType, inputArtifactsStore, liveosConfig.kdumpBootFiles, liveosConfig.additionalFiles, - liveosConfig.bootstrapBaseUrl, liveosConfig.bootstrapFileUrl, outputPath) + liveosConfig.bootstrapBaseUrl, liveosConfig.bootstrapFileUrl, outputPath, distroHandler) if err != nil { return fmt.Errorf("failed to generate PXE artifacts folder\n%w", err) } diff --git a/toolkit/tools/pkg/imagecustomizerlib/liveosisoutils.go b/toolkit/tools/pkg/imagecustomizerlib/liveosisoutils.go index fb44f31b82..83c284b765 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/liveosisoutils.go +++ b/toolkit/tools/pkg/imagecustomizerlib/liveosisoutils.go @@ -44,13 +44,12 @@ const ( initrdImage = "initrd.img" // In vhd(x)/qcow/iso images, the kernel is named 'vmlinuz-'. - vmLinuzPrefix = "vmlinuz-" - initramfsPrefix = "initramfs-" // AZL3, Fedora, etc. - initrdPrefix = "initrd.img-" // AZL2, Ubuntu, etc. - isoKernelDir = "/boot" - isoInitrdPath = "/boot/" + initrdImage - isoBootloadersDir = "/efi/boot" - isoBootImagePath = "/boot/grub2/efiboot.img" + vmLinuzPrefix = "vmlinuz-" + initramfsPrefix = "initramfs-" // AZL3, Fedora, etc. + initrdPrefix = "initrd.img-" // AZL2, Ubuntu, etc. + isoKernelDir = "/boot" + isoInitrdPath = "/boot/" + initrdImage + isoBootImagePath = "/boot/grub2/efiboot.img" // Minimum dracut version required to enable PXE booting. LiveOsPxeDracutMinVersion = 102 @@ -89,7 +88,7 @@ type BootFilesArchConfig struct { ukiAddonStubBinaryPath string } -var bootloaderFilesConfig = map[string]BootFilesArchConfig{ +var bootloaderFilesConfigAzureLinux = map[string]BootFilesArchConfig{ "amd64": { bootBinary: bootx64Binary, grubBinary: grubx64Binary, @@ -124,12 +123,15 @@ var bootloaderFilesConfig = map[string]BootFilesArchConfig{ }, } -func getBootArchConfig() (string, BootFilesArchConfig, error) { +// bootArchConfigFromMap looks up the current runtime architecture in the provided per-arch boot files config map. +func bootArchConfigFromMap(configByArch map[string]BootFilesArchConfig) (BootFilesArchConfig, error) { arch := runtime.GOARCH - if arch != "amd64" && arch != "arm64" { - return "", BootFilesArchConfig{}, fmt.Errorf("unsupported architecture: %s", arch) + switch arch { + case "amd64", "arm64": + return configByArch[arch], nil + default: + return BootFilesArchConfig{}, fmt.Errorf("unsupported architecture: %s", arch) } - return arch, bootloaderFilesConfig[arch], nil } // verifies that the dracut package supports PXE booting for LiveOS images. diff --git a/toolkit/tools/pkg/imagecustomizerlib/liveospxe.go b/toolkit/tools/pkg/imagecustomizerlib/liveospxe.go index de3fb0ab4c..11e73bddf6 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/liveospxe.go +++ b/toolkit/tools/pkg/imagecustomizerlib/liveospxe.go @@ -45,7 +45,7 @@ func getPxeBootstrapFileName(bootstrapBaseUrl, bootstrapFileUrl string) (string, func createPXEArtifacts(buildDir string, outputFormat imagecustomizerapi.ImageFormatType, initramfsType imagecustomizerapi.InitramfsImageType, artifactsStore *IsoArtifactsStore, kdumpBootFiles *imagecustomizerapi.KdumpBootFilesType, additionalIsoFiles imagecustomizerapi.AdditionalFileList, - bootstrapBaseUrl, bootstrapFileUrl, outputPath string) (err error) { + bootstrapBaseUrl, bootstrapFileUrl, outputPath string, distroHandler DistroHandler) (err error) { logger.Log.Infof("Creating PXE output at (%s)", outputPath) outputPXEArtifactsDir := "" @@ -116,12 +116,13 @@ func createPXEArtifacts(buildDir string, outputFormat imagecustomizerapi.ImageFo // created because some of these files are needed by the ISO so it is // bootable. - // Move bootloader files from under '/efi/boot' to '/' - _, bootFilesConfig, err := getBootArchConfig() + // Move bootloader files from under '/' to '/'. + bootFilesConfig, err := distroHandler.GetBootArchConfig() if err != nil { return err } - bootloaderSrcDir := filepath.Join(outputPXEArtifactsDir, isoBootloadersDir) + isoBootloaderDir := filepath.Dir(bootFilesConfig.isoBootBinaryPath) + bootloaderSrcDir := filepath.Join(outputPXEArtifactsDir, isoBootloaderDir) bootloaderFiles := []string{bootFilesConfig.bootBinary, bootFilesConfig.grubBinary} for _, bootloaderFile := range bootloaderFiles { @@ -133,8 +134,8 @@ func createPXEArtifacts(buildDir string, outputFormat imagecustomizerapi.ImageFo } } - // Remove the empty 'pxe-folder>/efi' folder. - isoEFIDir := filepath.Join(outputPXEArtifactsDir, "efi") + // Remove the now-empty parent of '/'. + isoEFIDir := filepath.Join(outputPXEArtifactsDir, filepath.Dir(isoBootloaderDir)) err = os.RemoveAll(isoEFIDir) if err != nil { return fmt.Errorf("failed to remove folder (%s):\n%w", isoEFIDir, err)