From 72f8a787209dd29bbf5226ec934a13030a31567c Mon Sep 17 00:00:00 2001 From: "Brian C. Lane" Date: Tue, 12 May 2026 11:56:41 -0700 Subject: [PATCH 01/12] iso_tree: Rewrite to use BootcRootFS and ISOBootloader list Remove the rootfs compression code, it is now created by BootcRootFS and is included in the treePipeline. Use the ISOBootloader list and common bootloader code from iso_bootloaders. This allows the iso to support other arches and bootloader configurations. Also adds optional support for setting the ostree= value in the grub.cfg file when SetOSTREE is true. KernelOpts must already contain 'ostree=@OSTREE@' if this option is used. It will add the org.osbuild.ostree.grub stage which mounts the rootfs.img, examines the ostree filesystem to extract the boot path and then substitute it for @OSTREE@ in the grub.cfg file. Related: HMS-10627 --- pkg/manifest/iso_tree.go | 267 +++++++-------------------- pkg/manifest/iso_tree_export_test.go | 7 + pkg/manifest/iso_tree_test.go | 116 ++++++++++++ 3 files changed, 190 insertions(+), 200 deletions(-) create mode 100644 pkg/manifest/iso_tree_export_test.go create mode 100644 pkg/manifest/iso_tree_test.go diff --git a/pkg/manifest/iso_tree.go b/pkg/manifest/iso_tree.go index 149cc94cdd..4012ed00b2 100644 --- a/pkg/manifest/iso_tree.go +++ b/pkg/manifest/iso_tree.go @@ -3,6 +3,7 @@ package manifest import ( "fmt" + "github.com/osbuild/image-builder/pkg/customizations/fsnode" "github.com/osbuild/image-builder/pkg/disk" "github.com/osbuild/image-builder/pkg/osbuild" ) @@ -17,102 +18,47 @@ type ISOTree struct { Version string PartitionTable *disk.PartitionTable + bootloaders []ISOBootloader + files []*fsnode.File - treePipeline Pipeline - bootTreePipeline *EFIBootTree + treePipeline TreePipeline - isoLabel string - - RootfsCompression string - RootfsType ISORootfsType - // set only when RootfsType is erofs - ErofsOptions *osbuild.ErofsStageOptions `yaml:"erofs_options,omitempty"` - - // Kernel options for the ISO image - KernelOpts []string - - // Kernel and initramfs paths in the tree pipeline + // Kernel, initramfs, and rootfs paths in the tree pipeline KernelPath string InitramfsPath string + RootfsPath string + + // Optionally set the ostree= argument (substitute the boot path for @OSTREE@ in grub.cfg) + SetOSTREE bool + // What is RootfsPath's file type, so it can be mounted for ostree examination + RootfsType ISORootfsType } -func NewISOTree(buildPipeline Build, treePipeline Pipeline, bootTreePipeline *EFIBootTree) *ISOTree { +func NewISOTree(buildPipeline Build, treePipeline TreePipeline, bootloaders []ISOBootloader) *ISOTree { // the pipelines should all belong to the same manifest - if treePipeline.Manifest() != bootTreePipeline.Manifest() { - panic("pipelines from different manifests") + for _, b := range bootloaders { + if b.Manifest() != nil { + if b.Manifest() != treePipeline.Manifest() { + panic("pipelines from different manifests") + } + } } + p := &ISOTree{ - Base: NewBase("bootiso-tree", buildPipeline), - treePipeline: treePipeline, - bootTreePipeline: bootTreePipeline, - isoLabel: bootTreePipeline.ISOLabel, + Base: NewBase("bootiso-tree", buildPipeline), + treePipeline: treePipeline, + bootloaders: bootloaders, } buildPipeline.addDependent(p) return p } -// NewSquashfsStage returns an osbuild stage configured to build -// the squashfs root filesystem for the ISO. -func (p *ISOTree) NewSquashfsStage() (*osbuild.Stage, error) { - // TODO: We should somehow make this customizable, because non-live anaconda ISOs - // use images/install.img - squashfsOptions := osbuild.SquashfsStageOptions{ - Filename: "LiveOS/squashfs.img", - } - - if p.RootfsCompression != "" { - squashfsOptions.Compression.Method = p.RootfsCompression - } else { - // default to xz if not specified - squashfsOptions.Compression.Method = "xz" - } - - if squashfsOptions.Compression.Method == "xz" { - // Try to get architecture from bootTreePipeline if available - arch := "x86_64" // default - if p.bootTreePipeline.Platform != nil { - arch = p.bootTreePipeline.Platform.GetArch().String() - } - squashfsOptions.Compression.Options = &osbuild.FSCompressionOptions{ - BCJ: osbuild.BCJOption(arch), - } - } - - // Clean up the root filesystem's /boot to save space - squashfsOptions.ExcludePaths = installerBootExcludePaths - - return osbuild.NewSquashfsStage(&squashfsOptions, p.treePipeline.Name()), nil -} - -// NewErofsStage returns an osbuild stage configured to build -// the erofs root filesystem for the ISO. -func (p *ISOTree) NewErofsStage() (*osbuild.Stage, error) { - // TODO: We should somehow make this customizable, because non-live anaconda ISOs - // use images/install.img - if p.RootfsType != ErofsRootfs || p.ErofsOptions == nil { - return nil, fmt.Errorf("Rootfs not set to Erofs or options set to nil for %s pipeline, can not create erofs stage", p.name) - } - - erofsOptions := *p.ErofsOptions - erofsOptions.Filename = "LiveOS/squashfs.img" - - // Clean up the root filesystem's /boot to save space - erofsOptions.ExcludePaths = installerBootExcludePaths - - return osbuild.NewErofsStage(erofsOptions, p.treePipeline.Name()), nil -} - func (p *ISOTree) serialize() (osbuild.Pipeline, error) { pipeline, err := p.Base.serialize() if err != nil { return osbuild.Pipeline{}, err } - kernelOpts := []string{} - if len(p.KernelOpts) > 0 { - kernelOpts = append(kernelOpts, p.KernelOpts...) - } - pipeline.AddStage(osbuild.NewMkdirStage(&osbuild.MkdirStageOptions{ Paths: []osbuild.MkdirStagePath{ { @@ -128,8 +74,8 @@ func (p *ISOTree) serialize() (osbuild.Pipeline, error) { })) // Copy kernel and initramfs - if p.KernelPath == "" || p.InitramfsPath == "" { - return osbuild.Pipeline{}, fmt.Errorf("kernel and initramfs paths must be set") + if p.KernelPath == "" || p.InitramfsPath == "" || p.RootfsPath == "" { + return osbuild.Pipeline{}, fmt.Errorf("kernel, initramfs, and rootfs paths must be set") } inputName := "tree" @@ -143,147 +89,68 @@ func (p *ISOTree) serialize() (osbuild.Pipeline, error) { From: fmt.Sprintf("input://%s/%s", inputName, p.InitramfsPath), To: "tree:///images/pxeboot/initrd.img", }, + { + From: fmt.Sprintf("input://%s/%s", inputName, p.RootfsPath), + To: "tree:///LiveOS/rootfs.img", + }, }, } copyStageInputs := osbuild.NewPipelineTreeInputs(inputName, p.treePipeline.Name()) copyStage := osbuild.NewCopyStageSimple(copyStageOptions, copyStageInputs) pipeline.AddStage(copyStage) - // Add the selected rootfs stage - switch p.RootfsType { - case SquashfsRootfs: - stage, err := p.NewSquashfsStage() + // Add bootloaders + for _, loader := range p.bootloaders { + stages, files, err := loader.GetISOBootStages(p.treePipeline.Name(), p.PartitionTable) if err != nil { - return osbuild.Pipeline{}, fmt.Errorf("cannot create squashfs stage: %w", err) + return osbuild.Pipeline{}, fmt.Errorf("cannot add ISO bootloader: %w", err) } - pipeline.AddStage(stage) - case ErofsRootfs: - stage, err := p.NewErofsStage() - if err != nil { - return osbuild.Pipeline{}, fmt.Errorf("cannot create erofs stage: %w", err) - } - pipeline.AddStage(stage) - default: - return osbuild.Pipeline{}, fmt.Errorf("unsupported rootfs type: %v (only SquashfsRootfs and ErofsRootfs are supported)", p.RootfsType) - } - - // Add grub2 boot configuration - product := p.Product - if product == "" { - product = "OS" - } - version := p.Version - if version == "" { - version = p.Release + pipeline.AddStages(stages...) + p.files = append(p.files, files...) } - var grub2config *osbuild.Grub2Config - var disableTestEntry bool - var disableTroubleshootingEntry bool - - if p.bootTreePipeline != nil { - if p.bootTreePipeline.DefaultMenu > 0 { - grub2config = &osbuild.Grub2Config{ - Default: p.bootTreePipeline.DefaultMenu, - } - } - - if p.bootTreePipeline.MenuTimeout != nil { - if grub2config == nil { - grub2config = &osbuild.Grub2Config{ - Timeout: *p.bootTreePipeline.MenuTimeout, - } - } else { - grub2config.Timeout = *p.bootTreePipeline.MenuTimeout - } + // Optional stage to set the ostree= value in the bootloader + // This is used for bootc/ostree rootfs images + if p.SetOSTREE { + lodevice := osbuild.NewLoopbackDevice( + &osbuild.LoopbackDeviceOptions{ + Filename: "LiveOS/rootfs.img", + }, + ) + devices := map[string]osbuild.Device{"disk": *lodevice} + var mounts []osbuild.Mount + switch p.RootfsType { + case ErofsRootfs: + mounts = []osbuild.Mount{*osbuild.NewErofsMount("-", "disk", "/")} + case SquashfsRootfs: + mounts = []osbuild.Mount{*osbuild.NewSquashfsMount("-", "disk", "/")} + default: + return osbuild.Pipeline{}, fmt.Errorf("Unknown ISOTree rootfs type: %v", p.RootfsType) } - disableTestEntry = p.bootTreePipeline.DisableTestEntry - disableTroubleshootingEntry = p.bootTreePipeline.DisableTroubleshootingEntry - } - options := &osbuild.Grub2ISOLegacyStageOptions{ - Product: osbuild.Product{ - Name: product, - Version: version, - }, - Kernel: osbuild.ISOKernel{ - Dir: "/images/pxeboot", - Opts: kernelOpts, - }, - ISOLabel: p.isoLabel, - FIPS: false, - Install: true, - Test: !disableTestEntry, - Troubleshooting: !disableTroubleshootingEntry, - Config: grub2config, - } - - // If any menu entries are defined we turn off all default - // entries and instead append our own - // entries only - if len(p.bootTreePipeline.MenuEntries) > 0 { - options.Troubleshooting = false - options.Test = false - options.Install = false - - for _, entry := range p.bootTreePipeline.MenuEntries { - options.Custom = append(options.Custom, osbuild.Grub2ISOLegacyCustomEntryOptions{ - Name: entry.Name, - Linux: entry.Linux, - Initrd: entry.Initrd, - }) + // Update the grub.cfg with the ostree boot uuid + ostreeStageOptions := &osbuild.OSTreeGrub2StageOptions{ + Filename: "/EFI/BOOT/grub.cfg", + Source: "mount://-/", } + pipeline.AddStage(osbuild.NewOSTreeGrub2MountsStage(ostreeStageOptions, nil, devices, mounts)) } - if p.bootTreePipeline != nil && p.bootTreePipeline.Platform != nil { - options.FIPS = p.bootTreePipeline.Platform.GetFIPSMenu() - } - - stage := osbuild.NewGrub2ISOLegacyStage(options) - pipeline.AddStage(stage) - - // Add a stage to create the eltorito.img file for grub2 BIOS boot support - pipeline.AddStage(osbuild.NewGrub2InstStage(osbuild.NewGrub2InstISO9660StageOption("images/eltorito.img", "/boot/grub2"))) - - // Create EFI boot partition - filename := "images/efiboot.img" - pipeline.AddStage(osbuild.NewTruncateStage(&osbuild.TruncateStageOptions{ - Filename: filename, - Size: fmt.Sprintf("%d", p.PartitionTable.Size), + pipeline.AddStage(osbuild.NewDiscinfoStage(&osbuild.DiscinfoStageOptions{ + BaseArch: p.treePipeline.Platform().GetArch().String(), + Release: p.Release, })) - for _, stage := range osbuild.GenFsStages(p.PartitionTable, filename, p.treePipeline.Name()) { - pipeline.AddStage(stage) - } + return pipeline, nil +} - inputName = "root-tree" - copyInputs := osbuild.NewPipelineTreeInputs(inputName, p.bootTreePipeline.Name()) - copyOptions, copyDevices, copyMounts := osbuild.GenCopyFSTreeOptions(inputName, p.bootTreePipeline.Name(), filename, p.PartitionTable) - pipeline.AddStage(osbuild.NewCopyStage(copyOptions, copyInputs, copyDevices, copyMounts)) +func (p *ISOTree) getInline() []string { + inlineData := []string{} - copyInputs = osbuild.NewPipelineTreeInputs(inputName, p.bootTreePipeline.Name()) - pipeline.AddStage(osbuild.NewCopyStageSimple( - &osbuild.CopyStageOptions{ - Paths: []osbuild.CopyStagePath{ - { - From: fmt.Sprintf("input://%s/EFI", inputName), - To: "tree:///", - }, - }, - }, - copyInputs, - )) - - // Determine architecture for discinfo - arch := "x86_64" // default - if p.bootTreePipeline != nil && p.bootTreePipeline.Platform != nil { - arch = p.bootTreePipeline.Platform.GetArch().String() + // inline data for custom files + for _, file := range p.files { + inlineData = append(inlineData, string(file.Data())) } - pipeline.AddStage(osbuild.NewDiscinfoStage(&osbuild.DiscinfoStageOptions{ - BaseArch: arch, - Release: p.Release, - })) - - return pipeline, nil + return inlineData } diff --git a/pkg/manifest/iso_tree_export_test.go b/pkg/manifest/iso_tree_export_test.go new file mode 100644 index 0000000000..fbea8eaa9f --- /dev/null +++ b/pkg/manifest/iso_tree_export_test.go @@ -0,0 +1,7 @@ +package manifest + +import "github.com/osbuild/image-builder/pkg/osbuild" + +func (it *ISOTree) Serialize() (osbuild.Pipeline, error) { + return it.serialize() +} diff --git a/pkg/manifest/iso_tree_test.go b/pkg/manifest/iso_tree_test.go new file mode 100644 index 0000000000..ab52ba55f8 --- /dev/null +++ b/pkg/manifest/iso_tree_test.go @@ -0,0 +1,116 @@ +package manifest_test + +import ( + "math/rand" + "testing" + + "github.com/osbuild/image-builder/pkg/arch" + "github.com/osbuild/image-builder/pkg/disk" + "github.com/osbuild/image-builder/pkg/manifest" + "github.com/osbuild/image-builder/pkg/osbuild" + "github.com/osbuild/image-builder/pkg/platform" + "github.com/osbuild/image-builder/pkg/runner" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// newTestISOTree returns a mock ISOTree pipeline for use in testing +// bootType controls which test bootloaders are created +// ostree controls whether or not the org.osbuild.ostree.grub stage is added +func makeFakeISOTree(bootType manifest.ISOBootType, ostree bool) *manifest.ISOTree { + m := &manifest.Manifest{} + runner := &runner.Linux{} + build := manifest.NewBuild(m, runner, nil, nil) + pf := &platform.Data{ + Arch: arch.ARCH_X86_64, + UEFIVendor: "test", + } + bootloaders := newTestBootloaders(bootType, build, pf, "test-iso", "1") + + rawBootcPipeline := manifest.NewRawBootcImage(build, nil, pf) + + rawBootcPipeline.KernelVersion = "kernel-7.0.8-100.fc43.x86_64" + rawBootcPipeline.LiveBoot = true + + rootfsPipeline := manifest.NewBootcRootFS(build, rawBootcPipeline, pf) + isoTree := manifest.NewISOTree(build, rootfsPipeline, bootloaders) + isoTree.KernelPath = "vmlinuz" + isoTree.InitramfsPath = "initrd.img" + isoTree.RootfsPath = "rootfs.img" + isoTree.SetOSTREE = ostree + + source := rand.NewSource(int64(0)) + // math/rand is good enough in this case + /* #nosec G404 */ + rng := rand.New(source) + isoTree.PartitionTable = disk.EFIBootPartitionTable(rng) + + return isoTree +} + +func TestISOTreeNoOSTREE(t *testing.T) { + isoTree := makeFakeISOTree(manifest.Grub2UEFIOnlyISOBoot, false) + + pipeline, err := isoTree.Serialize() + require.NoError(t, err) + + copyStages := findStages("org.osbuild.copy", pipeline.Stages) + require.Greater(t, len(copyStages), 0) + var fromPaths []string + var toPaths []string + for _, s := range copyStages { + copyOptions := s.Options.(*osbuild.CopyStageOptions) + for _, p := range copyOptions.Paths { + fromPaths = append(fromPaths, p.From) + toPaths = append(toPaths, p.To) + } + } + // Check for the kernel/initrd/rootfs from the ostree deployment + assert.Contains(t, fromPaths, "input://tree/vmlinuz") + assert.Contains(t, fromPaths, "input://tree/initrd.img") + assert.Contains(t, fromPaths, "input://tree/rootfs.img") + assert.Contains(t, fromPaths, "input://root-tree/EFI") + + // Check for final paths for kernel, initrd, rootfs + assert.Contains(t, toPaths, "tree:///images/pxeboot/vmlinuz") + assert.Contains(t, toPaths, "tree:///images/pxeboot/initrd.img") + assert.Contains(t, toPaths, "tree:///LiveOS/rootfs.img") + + // No ostree.grub2 stage + require.Nil(t, findStage("org.osbuild.ostree.grub2", pipeline.Stages)) +} + +func TestISOTreeWithSquashfsOSTREE(t *testing.T) { + isoTree := makeFakeISOTree(manifest.Grub2UEFIOnlyISOBoot, true) + isoTree.RootfsType = manifest.SquashfsRootfs + + pipeline, err := isoTree.Serialize() + require.NoError(t, err) + + // Check the ostree.grub2 stage + grub2Stage := findStage("org.osbuild.ostree.grub2", pipeline.Stages) + require.NotNil(t, grub2Stage) + grub2Opts := grub2Stage.Options.(*osbuild.OSTreeGrub2StageOptions) + assert.Equal(t, "/EFI/BOOT/grub.cfg", grub2Opts.Filename) + assert.Equal(t, "mount://-/", grub2Opts.Source) + // Check ostree.grub2 mount + assert.Equal(t, "org.osbuild.squashfs", grub2Stage.Mounts[0].Type) +} + +func TestISOTreeWithErofsOSTREE(t *testing.T) { + isoTree := makeFakeISOTree(manifest.Grub2UEFIOnlyISOBoot, true) + isoTree.RootfsType = manifest.ErofsRootfs + + pipeline, err := isoTree.Serialize() + require.NoError(t, err) + + // Check the ostree.grub2 stage + grub2Stage := findStage("org.osbuild.ostree.grub2", pipeline.Stages) + require.NotNil(t, grub2Stage) + grub2Opts := grub2Stage.Options.(*osbuild.OSTreeGrub2StageOptions) + assert.Equal(t, "/EFI/BOOT/grub.cfg", grub2Opts.Filename) + assert.Equal(t, "mount://-/", grub2Opts.Source) + // Check ostree.grub2 mount + assert.Equal(t, "org.osbuild.erofs", grub2Stage.Mounts[0].Type) +} From 0fbb8c5ca20dc1b64be1861d77827842e8d1fa03 Mon Sep 17 00:00:00 2001 From: "Brian C. Lane" Date: Fri, 12 Jun 2026 09:45:50 -0700 Subject: [PATCH 02/12] iso_tree: Use LiveOS/squashfs.img on the iso dmsquash-live-root treats squashfs.img and rootfs.img differently, it uses the same codepath as http root for squashfs.img which is where the root filesystem detection (looking for /usr or /ostree) is triggered. If rootfs.img is used dracut sets up the root using device-mapper instead of overlay, and this does not work with ostree. Related: HMS-10627 --- pkg/manifest/iso_tree.go | 4 ++-- pkg/manifest/iso_tree_test.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/manifest/iso_tree.go b/pkg/manifest/iso_tree.go index 4012ed00b2..c12b3ac00c 100644 --- a/pkg/manifest/iso_tree.go +++ b/pkg/manifest/iso_tree.go @@ -91,7 +91,7 @@ func (p *ISOTree) serialize() (osbuild.Pipeline, error) { }, { From: fmt.Sprintf("input://%s/%s", inputName, p.RootfsPath), - To: "tree:///LiveOS/rootfs.img", + To: "tree:///LiveOS/squashfs.img", }, }, } @@ -114,7 +114,7 @@ func (p *ISOTree) serialize() (osbuild.Pipeline, error) { if p.SetOSTREE { lodevice := osbuild.NewLoopbackDevice( &osbuild.LoopbackDeviceOptions{ - Filename: "LiveOS/rootfs.img", + Filename: "LiveOS/squashfs.img", }, ) devices := map[string]osbuild.Device{"disk": *lodevice} diff --git a/pkg/manifest/iso_tree_test.go b/pkg/manifest/iso_tree_test.go index ab52ba55f8..da6beb053f 100644 --- a/pkg/manifest/iso_tree_test.go +++ b/pkg/manifest/iso_tree_test.go @@ -72,10 +72,10 @@ func TestISOTreeNoOSTREE(t *testing.T) { assert.Contains(t, fromPaths, "input://tree/rootfs.img") assert.Contains(t, fromPaths, "input://root-tree/EFI") - // Check for final paths for kernel, initrd, rootfs + // Check for final paths for kernel, initrd, rootfs (squashfs.img) assert.Contains(t, toPaths, "tree:///images/pxeboot/vmlinuz") assert.Contains(t, toPaths, "tree:///images/pxeboot/initrd.img") - assert.Contains(t, toPaths, "tree:///LiveOS/rootfs.img") + assert.Contains(t, toPaths, "tree:///LiveOS/squashfs.img") // No ostree.grub2 stage require.Nil(t, findStage("org.osbuild.ostree.grub2", pipeline.Stages)) From 7eb8f5bd81683f3457930c1e97bf7806eefb2f62 Mon Sep 17 00:00:00 2001 From: "Brian C. Lane" Date: Fri, 8 May 2026 14:01:42 -0700 Subject: [PATCH 03/12] bootc-generic-iso: Add partition information and erofs settings The partition is needed for installing bootc to a partitioned disk image, and the erofs settings are used for the rootfs of the iso. Related: HMS-10627 --- data/distrodefs/bootc-generic/imagetypes.yaml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/data/distrodefs/bootc-generic/imagetypes.yaml b/data/distrodefs/bootc-generic/imagetypes.yaml index 20365d81fc..dfeb38a01f 100644 --- a/data/distrodefs/bootc-generic/imagetypes.yaml +++ b/data/distrodefs/bootc-generic/imagetypes.yaml @@ -85,6 +85,14 @@ - *default_partition_table_part_boot - *default_partition_table_part_root + erofs_options: &default_erofs_options + compression: + method: "lzma" + level: 6 + options: + - "fragments" + cluster-size: 1048576 + supported_options_lists: supported_options_disk: &supported_options_disk - "customizations.directories" @@ -182,11 +190,15 @@ image_types: # TODO: Discuss names of container-based isos: anaconda-iso, # bootc-generic-iso and bootc-installer are all confusing. bootc-generic-iso: + <<: *raw_image_type mime_type: "application/x-iso9660-image" exports: ["bootiso"] filename: "install.iso" boot_iso: true image_func: "bootc_generic_iso" + iso_config: + rootfs_type: "erofs" + erofs_options: *default_erofs_options blueprint: supported_options: *supported_options_iso From a2e79c8db3be6ec1b5fac642586692a7b75af874 Mon Sep 17 00:00:00 2001 From: "Brian C. Lane" Date: Fri, 8 May 2026 14:04:01 -0700 Subject: [PATCH 04/12] container_based_iso: Use bootc container directly This changes to using the bootc container directly instead of the deployed filesystem. It builds on top of BootcRootFS (also used by the bootc PXE tar image type). It uses the iso and installer customizations from the bootc distro YAML for setting up the rootfs compression and optional kernel arguments. It also uses the common bootloaders functions which should make it easier to eventually add support for other platforms. Related: HMS-10627 --- pkg/image/container_based_iso.go | 140 +++++++++++++++---------------- 1 file changed, 67 insertions(+), 73 deletions(-) diff --git a/pkg/image/container_based_iso.go b/pkg/image/container_based_iso.go index 7013703470..af9af2e647 100644 --- a/pkg/image/container_based_iso.go +++ b/pkg/image/container_based_iso.go @@ -8,6 +8,7 @@ import ( "github.com/osbuild/image-builder/pkg/container" "github.com/osbuild/image-builder/pkg/disk" "github.com/osbuild/image-builder/pkg/manifest" + "github.com/osbuild/image-builder/pkg/osbuild" "github.com/osbuild/image-builder/pkg/platform" "github.com/osbuild/image-builder/pkg/runner" ) @@ -15,30 +16,23 @@ import ( type ContainerBasedIso struct { Base - // Container source for the OS tree - ContainerSource container.SourceSpec - - // PayloadContainer is an optional container to embed in the image's - // container storage (for bootc installer scenarios where the payload - // container needs to be available at install time). - PayloadContainer *container.SourceSpec - - Product string - Version string - Release string + PartitionTable *disk.PartitionTable - ISOLabel string + // Customizations + OSCustomizations manifest.OSCustomizations + DiskCustomizations manifest.DiskCustomizations + ISOCustomizations manifest.ISOCustomizations + InstallerCustomizations manifest.InstallerCustomizations - RootfsCompression string - RootfsType manifest.ISORootfsType + // Kernel version from the container, used to copy it into the PXE tar tree + KernelVersion string - KernelPath string - KernelOpts []string - InitramfsPath string + // Container source for the OS tree + ContainerSource container.SourceSpec - Grub2MenuDefault *int - Grub2MenuTimeout *int + KernelOpts []string + // Custom ISO bootloader menus override the default menus Grub2MenuEntries []manifest.ISOGrub2MenuEntry } @@ -46,7 +40,21 @@ func NewContainerBasedIso(platform platform.Platform, filename string, container return &ContainerBasedIso{ Base: NewBase("container-based-iso", platform, filename), ContainerSource: container, + DiskCustomizations: manifest.DiskCustomizations{ + MountConfiguration: osbuild.MOUNT_CONFIGURATION_NONE, // default to no mount config for PXE images + }, + } +} + +// Bootloaders returns the list of configured bootloaders for the platform +func (img *ContainerBasedIso) Bootloaders(buildPipeline manifest.Build, kernelOpts []string) []manifest.ISOBootloader { + ibo := ISOBootloaders{ + InstallerCustomizations: &img.InstallerCustomizations, + ISOCustomizations: &img.ISOCustomizations, + Custom: img.Grub2MenuEntries, } + + return ibo.Bootloaders(buildPipeline, img.platform, kernelOpts) } func (img *ContainerBasedIso) InstantiateManifestFromContainer(m *manifest.Manifest, @@ -55,71 +63,57 @@ func (img *ContainerBasedIso) InstantiateManifestFromContainer(m *manifest.Manif rng *rand.Rand) (*artifact.Artifact, error) { cnts := []container.SourceSpec{img.ContainerSource} - kernelOpts := []string{} - - if len(img.KernelOpts) > 0 { - kernelOpts = append(kernelOpts, img.KernelOpts...) - } else { - // org.osbuild.grub2.iso.legacy fails to run with empty kernel options, so let's use something harmless - // if we didn't have any specific kernelopts set - kernelOpts = []string{ - fmt.Sprintf("root=live:CDLABEL=%s", img.ISOLabel), - "rd.live.image", - "quiet", - "rhgb", - "enforcing=0", - } - } - buildOptions := img.BuildOptions if buildOptions == nil { buildOptions = &manifest.BuildOptions{} } buildOptions.ContainerBuildable = true buildPipeline := manifest.NewBuildFromContainer(m, runner, cnts, buildOptions) - osTreePipeline := manifest.NewOSFromContainer("os-tree", buildPipeline, &img.ContainerSource) - osTreePipeline.PayloadContainer = img.PayloadContainer - product := img.Product - if product == "" { - product = "OS" - } - version := img.Version - if version == "" { - version = img.Release - } - bootTreePipeline := manifest.NewEFIBootTree(buildPipeline, product, version) - bootTreePipeline.Platform = img.platform - bootTreePipeline.UEFIVendor = img.platform.GetUEFIVendor() - bootTreePipeline.ISOLabel = img.ISOLabel - bootTreePipeline.KernelOpts = kernelOpts - bootTreePipeline.MenuTimeout = img.Grub2MenuTimeout - bootTreePipeline.DisableTestEntry = true - bootTreePipeline.DisableTroubleshootingEntry = true - - if img.Grub2MenuDefault != nil { - bootTreePipeline.DefaultMenu = *img.Grub2MenuDefault + rawImage := manifest.NewRawBootcImage(buildPipeline, containers, img.platform) + rawImage.PartitionTable = img.PartitionTable + rawImage.OSCustomizations = img.OSCustomizations + rawImage.DiskCustomizations = img.DiskCustomizations + rawImage.KernelVersion = img.KernelVersion + + // Setup root filesystem so that dmsquash-live will boot it + rawImage.LiveBoot = true + + rootfsPipeline := manifest.NewBootcRootFS(buildPipeline, rawImage, img.platform) + rootfsPipeline.ErofsOptions = img.ISOCustomizations.ErofsOptions + rootfsPipeline.RootfsType = img.ISOCustomizations.RootfsType + + // Setup the boot args for a live iso with ostree rootfs + // The @OSTREE@ entry is substituted by the org.osbuild.ostree.grub2 stage + // after inspecting the rootfs.img for the ostree default boot target's path. + // this requires the ISOTree bool SetOSTREE to be true. + kernelOpts := []string{ + // TODO escape the label (or make sure it is escaped earlier) + fmt.Sprintf("root=live:CDLABEL=%s", img.ISOCustomizations.Label), + "ostree=@OSTREE@", + "rd.live.image", + "fstab=no", // Prevents systemd-fstab-generator from making mount units + "quiet", + "rhgb", } + kernelOpts = append(kernelOpts, img.KernelOpts...) + kernelOpts = append(kernelOpts, img.InstallerCustomizations.KernelOptionsAppend...) - bootTreePipeline.MenuEntries = img.Grub2MenuEntries + // Setup the bootloaders + bootloaders := img.Bootloaders(buildPipeline, kernelOpts) - isoTreePipeline := manifest.NewISOTree(buildPipeline, osTreePipeline, bootTreePipeline) + isoTreePipeline := manifest.NewISOTree(buildPipeline, rootfsPipeline, bootloaders) isoTreePipeline.PartitionTable = disk.EFIBootPartitionTable(rng) - isoTreePipeline.Release = img.Release - isoTreePipeline.Product = img.Product - isoTreePipeline.Version = img.Version - isoTreePipeline.RootfsCompression = img.RootfsCompression - isoTreePipeline.RootfsType = img.RootfsType - isoTreePipeline.KernelPath = img.KernelPath - isoTreePipeline.InitramfsPath = img.InitramfsPath - isoTreePipeline.KernelOpts = kernelOpts - - isoCustomizations := manifest.ISOCustomizations{ - Label: img.ISOLabel, - BootType: manifest.Grub2ISOBoot, - } - - isoPipeline := manifest.NewISO(buildPipeline, isoTreePipeline, isoCustomizations) + isoTreePipeline.Release = img.InstallerCustomizations.Release + // Paths are relative to the rootfsPipeline tree root + isoTreePipeline.KernelPath = "vmlinuz" + isoTreePipeline.InitramfsPath = "initrd.img" + isoTreePipeline.RootfsPath = "rootfs.img" + // Mount the rootfs, get the ostree boot path, update grub.cfg ostree= entry + isoTreePipeline.SetOSTREE = true + isoTreePipeline.RootfsType = rootfsPipeline.RootfsType + + isoPipeline := manifest.NewISO(buildPipeline, isoTreePipeline, img.ISOCustomizations) isoPipeline.SetFilename(img.filename) artifact := isoPipeline.Export() From 01ae16813ddf5de8af9e9732c6f5e092c4f3f123 Mon Sep 17 00:00:00 2001 From: "Brian C. Lane" Date: Tue, 5 May 2026 13:40:59 -0700 Subject: [PATCH 05/12] bootc_imagetype: Use new container_based_iso This starts by making sure the required modules have been included in the container's initrd. It also hooks up the blueprint customizations, and uses the YAML for the iso configuration (erofs or squashfs rootfs type). Related: HMS-10627 --- pkg/distro/generic/bootc_imagetype.go | 126 ++++++++++++++---- pkg/distro/generic/bootc_test.go | 18 ++- test/bootc-fake-containers.yaml | 4 + test/config-list.json | 6 +- ...os_1-aarch64-bootc_generic_iso-bootc_empty | 2 +- ...rch64-bootc_generic_iso-bootc_remote_empty | 2 +- ...ootc_generic_iso-bootc_remote_with_payload | 1 - ...rch64-bootc_generic_iso-bootc_with_payload | 1 - ..._os_1-x86_64-bootc_generic_iso-bootc_empty | 2 +- ...86_64-bootc_generic_iso-bootc_remote_empty | 2 +- ...ootc_generic_iso-bootc_remote_with_payload | 1 - ...86_64-bootc_generic_iso-bootc_with_payload | 1 - 12 files changed, 123 insertions(+), 43 deletions(-) delete mode 100644 test/data/manifest-checksums/bootc_test_os_1-aarch64-bootc_generic_iso-bootc_remote_with_payload delete mode 100644 test/data/manifest-checksums/bootc_test_os_1-aarch64-bootc_generic_iso-bootc_with_payload delete mode 100644 test/data/manifest-checksums/bootc_test_os_1-x86_64-bootc_generic_iso-bootc_remote_with_payload delete mode 100644 test/data/manifest-checksums/bootc_test_os_1-x86_64-bootc_generic_iso-bootc_with_payload diff --git a/pkg/distro/generic/bootc_imagetype.go b/pkg/distro/generic/bootc_imagetype.go index f621d5cc4c..1563d93d1c 100644 --- a/pkg/distro/generic/bootc_imagetype.go +++ b/pkg/distro/generic/bootc_imagetype.go @@ -37,7 +37,8 @@ var _ = distro.ImageType(&bootcImageType{}) type bootcImageType struct { defs.ImageTypeYAML - arch *architecture + arch *architecture + isoLabel string } func (t *bootcImageType) Name() string { @@ -65,7 +66,7 @@ func (t *bootcImageType) OSTreeRef() string { } func (t *bootcImageType) ISOLabel() (string, error) { - return "", nil + return t.isoLabel, nil } func (t *bootcImageType) Size(size uint64) uint64 { @@ -172,7 +173,7 @@ func (t *bootcImageType) manifestWithoutValidation(bp *blueprint.Blueprint, opti case "bootc_iso": return t.manifestForISO(bp, options, rng) case "bootc_generic_iso": - return t.manifestForGenericISO(options, rng) + return t.manifestForGenericISO(bp, options, rng) case "bootc_disk": return t.manifestForDisk(bp, options, rng) case "pxe_tar": @@ -441,11 +442,31 @@ func (t *bootcImageType) manifestForISO(bp *blueprint.Blueprint, options distro. return &mf, nil, err } -func (t *bootcImageType) manifestForGenericISO(options distro.ImageOptions, rng *rand.Rand) (*manifest.Manifest, []string, error) { +func (t *bootcImageType) manifestForGenericISO(bp *blueprint.Blueprint, options distro.ImageOptions, rng *rand.Rand) (*manifest.Manifest, []string, error) { bd := t.arch.distro.(*BootcDistro) if bd.imgref == "" { return nil, nil, fmt.Errorf("internal error: no base image defined") } + if bd.sourceInfo == nil { + return nil, nil, fmt.Errorf("internal error: Missing sourceInfo") + } + + // The bootc initramfs requires the dmsquash-live and ostree modules in order to boot + // check for them here so that we can tell the user instead of failing to boot + ok, err := bd.sourceInfo.HasModules([]string{"ostree", "dmsquash-live", "livenet"}) + if err != nil { + return nil, nil, fmt.Errorf("internal error: %w", err) + } + if !ok { + return nil, nil, fmt.Errorf("bootc container initramfs requires ostree, dmsquash-live and livenet modules") + } + + // Set the iso label, must be done before calling isoCustomizations + if label := bd.sourceInfo.ISOInfo.Label; label != "" { + t.isoLabel = label + } else { + t.isoLabel = bootc.LabelForISO(&bd.sourceInfo.OSRelease, t.arch.Name()) + } local := t.useLocalStorage(options) containerSource := container.SourceSpec{ @@ -458,44 +479,99 @@ func (t *bootcImageType) manifestForGenericISO(options distro.ImageOptions, rng platformi.ImageFormat = platform.FORMAT_ISO img := image.NewContainerBasedIso(platformi, t.Filename(), containerSource, nil) - if options.Bootc != nil && options.Bootc.InstallerPayloadRef != "" { - img.PayloadContainer = &container.SourceSpec{ - Source: options.Bootc.InstallerPayloadRef, - Name: options.Bootc.InstallerPayloadRef, - Local: local, - } + var customizations *blueprint.Customizations + if bp != nil { + customizations = bp.Customizations } - img.RootfsCompression = "zstd" - img.RootfsType = manifest.SquashfsRootfs + // NOTE: Only the / partition is needed since the final result is compressed + // filesystem. But the intermediate bootc filesystem install needs a size + // and partitions. + rootfsMinSize := max(bd.rootfsMinSize, options.Size) + pt, err := t.genPartitionTable(customizations, rootfsMinSize, rng) + if err != nil { + return nil, nil, err + } + img.PartitionTable = pt + + // Setup OSCustomizations + img.OSCustomizations.Users = users.UsersFromBP(customizations.GetUsers()) + groups, err := customizations.GetGroups() + if err != nil { + return nil, nil, err + } + img.OSCustomizations.Groups = users.GroupsFromBP(groups) + img.OSCustomizations.SELinux = bd.sourceInfo.SELinuxPolicy + img.OSCustomizations.BuildSELinux = img.OSCustomizations.SELinux + + if bd.sourceInfo != nil && bd.sourceInfo.MountConfiguration != nil { + img.DiskCustomizations.MountConfiguration = *bd.sourceInfo.MountConfiguration + } + + imageConfig := t.ImageTypeYAML.ImageConfig(bd.id, t.arch.Name()) + if imageConfig != nil { + img.OSCustomizations.KernelOptionsAppend = imageConfig.KernelOptions + } + if kopts := customizations.GetKernel(); kopts != nil && kopts.Append != "" { + img.OSCustomizations.KernelOptionsAppend = append(img.OSCustomizations.KernelOptionsAppend, kopts.Append) + } + + // Check Directory/File Customizations are valid + dc := customizations.GetDirectories() + fc := customizations.GetFiles() + if err := blueprint.ValidateDirFileCustomizations(dc, fc); err != nil { + return nil, nil, err + } + if err := blueprint.CheckDirectoryCustomizationsPolicy(dc, policies.OstreeCustomDirectoriesPolicies); err != nil { + return nil, nil, err + } + if err := blueprint.CheckFileCustomizationsPolicy(fc, policies.OstreeCustomFilesPolicies); err != nil { + return nil, nil, err + } + img.OSCustomizations.Files, err = blueprint.FileCustomizationsToFsNodeFiles(fc) + if err != nil { + return nil, nil, err + } + img.OSCustomizations.Directories, err = blueprint.DirectoryCustomizationsToFsNodeDirectories(dc) + if err != nil { + return nil, nil, err + } // Potentially KernelInfo is nil when we couldn't read it from the container; handle that so // we don't panic if bd.sourceInfo.KernelInfo == nil { return nil, nil, fmt.Errorf("could not read kernel info from container") } - img.KernelPath = fmt.Sprintf("lib/modules/%s/vmlinuz", bd.sourceInfo.KernelInfo.Version) - img.InitramfsPath = fmt.Sprintf("lib/modules/%s/initramfs.img", bd.sourceInfo.KernelInfo.Version) - img.Product = bd.sourceInfo.OSRelease.Name - img.Version = bd.sourceInfo.OSRelease.VersionID - img.Release = bd.sourceInfo.OSRelease.VersionID + img.KernelVersion = bd.sourceInfo.KernelInfo.Version // needed by BootcRootFS - isoi := bd.sourceInfo.ISOInfo + if options.Bootc != nil && options.Bootc.InstallerPayloadRef != "" { + return nil, nil, fmt.Errorf("bootc installer payload is not supported on generic-iso") + } - if isoi.Label != "" { - img.ISOLabel = isoi.Label - } else { - img.ISOLabel = bootc.LabelForISO(&bd.sourceInfo.OSRelease, t.arch.Name()) + // Setup the iso customizations from the YAML + img.ISOCustomizations, err = isoCustomizations(t, nil) + if err != nil { + return nil, nil, err + } + img.InstallerCustomizations = manifest.InstallerCustomizations{ + Product: bd.sourceInfo.OSRelease.Name, + OSVersion: bd.sourceInfo.OSRelease.VersionID, + Release: bd.sourceInfo.OSRelease.VersionID, } + isoi := bd.sourceInfo.ISOInfo + if isoi.Grub2.Default != nil { + // TODO This really belongs in ISOCustomization + img.InstallerCustomizations.DefaultMenu = *isoi.Grub2.Default + } + // TODO -- add timeout to one of the customizations + // img.Grub2MenuTimeout = isoi.Grub2.Timeout + if len(isoi.KernelArgs) > 0 { img.KernelOpts = isoi.KernelArgs } - img.Grub2MenuDefault = isoi.Grub2.Default - img.Grub2MenuTimeout = isoi.Grub2.Timeout img.Grub2MenuEntries = []manifest.ISOGrub2MenuEntry{} - for _, entry := range isoi.Grub2.Entries { img.Grub2MenuEntries = append(img.Grub2MenuEntries, manifest.ISOGrub2MenuEntry{ Name: entry.Name, diff --git a/pkg/distro/generic/bootc_test.go b/pkg/distro/generic/bootc_test.go index f0ff05147e..e2abe2efee 100644 --- a/pkg/distro/generic/bootc_test.go +++ b/pkg/distro/generic/bootc_test.go @@ -726,7 +726,7 @@ var isoContainers = map[string][]container.Spec{ "build": { containerSpec, }, - "os-tree": { + "image": { containerSpec, }, } @@ -983,12 +983,15 @@ func TestBootcIsoManifestSerialization(t *testing.T) { assert.NoError(t, err) manifestJson, err := mf.Serialize(nil, isoContainers, nil, nil, nil) - assert.NoError(t, err) + require.NoError(t, err) expStages := map[string][]string{ - "build": {"org.osbuild.container-deploy"}, - "os-tree": {"org.osbuild.container-deploy"}, - "bootiso": {"org.osbuild.xorrisofs"}, + "build": {"org.osbuild.container-deploy"}, + "image": {"org.osbuild.bootc.install-to-filesystem"}, + "bootc-rootfs": {"org.osbuild.erofs"}, + "efiboot-tree": {"org.osbuild.grub2.iso"}, + "bootiso-tree": {"org.osbuild.ostree.grub2"}, + "bootiso": {"org.osbuild.xorrisofs"}, } assert.NoError(t, checkStages(manifestJson, expStages, nil)) } @@ -1009,8 +1012,11 @@ func TestContainerSourceLocality(t *testing.T) { // definition via newDistroYAMLFrom() to find installer // packages. The generic test distro doesn't carry real OS // release data, so this image type cannot be tested here. - if imgTypeName == "anaconda-iso" || imgTypeName == "iso" { + switch imgTypeName { + case "anaconda-iso", "iso": t.Skipf("skipping %s: legacy ISO requires real distro definitions not available in the test distro", imgTypeName) + case "bootc-generic-iso": + t.Skipf("skipping %s: installer payload not supported ", imgTypeName) } imgOptions := distro.ImageOptions{ diff --git a/test/bootc-fake-containers.yaml b/test/bootc-fake-containers.yaml index c157c04e18..c113198273 100644 --- a/test/bootc-fake-containers.yaml +++ b/test/bootc-fake-containers.yaml @@ -11,6 +11,10 @@ containers: versionid: "1" kernel_info: version: 6.6 + initrd_modules: + - ostree + - livenet + - dmsquash-live default_fs: "ext4" container_size: 10_000_000_000 - <<: *default_fake_container diff --git a/test/config-list.json b/test/config-list.json index bb0552bd39..7bec3e62aa 100644 --- a/test/config-list.json +++ b/test/config-list.json @@ -751,8 +751,7 @@ "bootc-*" ], "image-types": [ - "bootc-installer", - "bootc-generic-iso" + "bootc-installer" ] } }, @@ -783,8 +782,7 @@ "bootc-*" ], "image-types": [ - "bootc-installer", - "bootc-generic-iso" + "bootc-installer" ] } }, diff --git a/test/data/manifest-checksums/bootc_test_os_1-aarch64-bootc_generic_iso-bootc_empty b/test/data/manifest-checksums/bootc_test_os_1-aarch64-bootc_generic_iso-bootc_empty index 97480fa98e..9a752217ea 100644 --- a/test/data/manifest-checksums/bootc_test_os_1-aarch64-bootc_generic_iso-bootc_empty +++ b/test/data/manifest-checksums/bootc_test_os_1-aarch64-bootc_generic_iso-bootc_empty @@ -1 +1 @@ -63d0d259ee0e5b361301b3a4f76abf5eb938fd5699f47b5e911a58afe99cedd7 +2f29b095806ba76886ac0efa8b433feb53efd17786584db40604092dfb7eb686 diff --git a/test/data/manifest-checksums/bootc_test_os_1-aarch64-bootc_generic_iso-bootc_remote_empty b/test/data/manifest-checksums/bootc_test_os_1-aarch64-bootc_generic_iso-bootc_remote_empty index a7908f8d4b..32da8a7f3e 100644 --- a/test/data/manifest-checksums/bootc_test_os_1-aarch64-bootc_generic_iso-bootc_remote_empty +++ b/test/data/manifest-checksums/bootc_test_os_1-aarch64-bootc_generic_iso-bootc_remote_empty @@ -1 +1 @@ -2f6f38a83cb1581a900684d97ad37e6ccf674a364828e31efd0974744adc0243 +3769899b7518761b5cf32d730e9b63472c16303230a514577d177086b9591cb4 diff --git a/test/data/manifest-checksums/bootc_test_os_1-aarch64-bootc_generic_iso-bootc_remote_with_payload b/test/data/manifest-checksums/bootc_test_os_1-aarch64-bootc_generic_iso-bootc_remote_with_payload deleted file mode 100644 index bdf605b441..0000000000 --- a/test/data/manifest-checksums/bootc_test_os_1-aarch64-bootc_generic_iso-bootc_remote_with_payload +++ /dev/null @@ -1 +0,0 @@ -86da72d87b728c80df7913e48cb43b91304d37bdacd65a13b02988f973a61537 diff --git a/test/data/manifest-checksums/bootc_test_os_1-aarch64-bootc_generic_iso-bootc_with_payload b/test/data/manifest-checksums/bootc_test_os_1-aarch64-bootc_generic_iso-bootc_with_payload deleted file mode 100644 index a60a6a414b..0000000000 --- a/test/data/manifest-checksums/bootc_test_os_1-aarch64-bootc_generic_iso-bootc_with_payload +++ /dev/null @@ -1 +0,0 @@ -a3b1cd6a627ebcea95107f3b9722085e47822d47ef3cd16626005c56e742c035 diff --git a/test/data/manifest-checksums/bootc_test_os_1-x86_64-bootc_generic_iso-bootc_empty b/test/data/manifest-checksums/bootc_test_os_1-x86_64-bootc_generic_iso-bootc_empty index d153b65939..37ea352513 100644 --- a/test/data/manifest-checksums/bootc_test_os_1-x86_64-bootc_generic_iso-bootc_empty +++ b/test/data/manifest-checksums/bootc_test_os_1-x86_64-bootc_generic_iso-bootc_empty @@ -1 +1 @@ -6887fd245ac21a4d340c26092e91fb32968dc2aa010234d8f94e3792ec12fac8 +d145f7012b97df0259734d9ef456c18a2b61c02ef6ef5118041b8312faa865a7 diff --git a/test/data/manifest-checksums/bootc_test_os_1-x86_64-bootc_generic_iso-bootc_remote_empty b/test/data/manifest-checksums/bootc_test_os_1-x86_64-bootc_generic_iso-bootc_remote_empty index c63b902101..76ca740bf1 100644 --- a/test/data/manifest-checksums/bootc_test_os_1-x86_64-bootc_generic_iso-bootc_remote_empty +++ b/test/data/manifest-checksums/bootc_test_os_1-x86_64-bootc_generic_iso-bootc_remote_empty @@ -1 +1 @@ -cac05dc1f31928bd0126801b6f3c3afb81f87658df25f35961a5d1780829b6a7 +2196a8c520e9ea7ecb744201dc89521657d4ab47c628e44ba0327ca972808d11 diff --git a/test/data/manifest-checksums/bootc_test_os_1-x86_64-bootc_generic_iso-bootc_remote_with_payload b/test/data/manifest-checksums/bootc_test_os_1-x86_64-bootc_generic_iso-bootc_remote_with_payload deleted file mode 100644 index fdef5a0984..0000000000 --- a/test/data/manifest-checksums/bootc_test_os_1-x86_64-bootc_generic_iso-bootc_remote_with_payload +++ /dev/null @@ -1 +0,0 @@ -0fa375e73b1872388e9cc1e0585f31796b9bb08b1805a7f53dfaba7d3c3b6d6a diff --git a/test/data/manifest-checksums/bootc_test_os_1-x86_64-bootc_generic_iso-bootc_with_payload b/test/data/manifest-checksums/bootc_test_os_1-x86_64-bootc_generic_iso-bootc_with_payload deleted file mode 100644 index 1fd5580995..0000000000 --- a/test/data/manifest-checksums/bootc_test_os_1-x86_64-bootc_generic_iso-bootc_with_payload +++ /dev/null @@ -1 +0,0 @@ -5064e0ceca39e60554b6edb1687089a58ac7a732b535a7da3cdec2df91ea14de From 2720e32a4ec4013be966ee7f1579a1f31d791da0 Mon Sep 17 00:00:00 2001 From: "Brian C. Lane" Date: Wed, 13 May 2026 16:50:42 -0700 Subject: [PATCH 06/12] bootc-generic-iso: Drop the bootc- part of the name It's redundant and makes the ibcli image names longer than they need to be. Keep bootc-generic-iso as an alias. Related: HMS-10627 --- data/distrodefs/bootc-generic/imagetypes.yaml | 8 ++++++-- pkg/distro/generic/bootc_test.go | 6 +++--- test/bootc-fake-containers.yaml | 6 +++--- test/config-list.json | 4 ++-- ...ty => bootc_test_os_1-aarch64-generic_iso-bootc_empty} | 0 ...ootc_test_os_1-aarch64-generic_iso-bootc_remote_empty} | 0 ...pty => bootc_test_os_1-x86_64-generic_iso-bootc_empty} | 0 ...bootc_test_os_1-x86_64-generic_iso-bootc_remote_empty} | 0 test/scripts/imgtestlib/boot.py | 2 +- 9 files changed, 15 insertions(+), 11 deletions(-) rename test/data/manifest-checksums/{bootc_test_os_1-aarch64-bootc_generic_iso-bootc_empty => bootc_test_os_1-aarch64-generic_iso-bootc_empty} (100%) rename test/data/manifest-checksums/{bootc_test_os_1-aarch64-bootc_generic_iso-bootc_remote_empty => bootc_test_os_1-aarch64-generic_iso-bootc_remote_empty} (100%) rename test/data/manifest-checksums/{bootc_test_os_1-x86_64-bootc_generic_iso-bootc_empty => bootc_test_os_1-x86_64-generic_iso-bootc_empty} (100%) rename test/data/manifest-checksums/{bootc_test_os_1-x86_64-bootc_generic_iso-bootc_remote_empty => bootc_test_os_1-x86_64-generic_iso-bootc_remote_empty} (100%) diff --git a/data/distrodefs/bootc-generic/imagetypes.yaml b/data/distrodefs/bootc-generic/imagetypes.yaml index dfeb38a01f..d7cd7893ed 100644 --- a/data/distrodefs/bootc-generic/imagetypes.yaml +++ b/data/distrodefs/bootc-generic/imagetypes.yaml @@ -188,8 +188,8 @@ image_types: # image. Additionally, the image type is not tied to anaconda as # bootc-installer is. This allows users to create a very customized ISOs. # TODO: Discuss names of container-based isos: anaconda-iso, - # bootc-generic-iso and bootc-installer are all confusing. - bootc-generic-iso: + # generic-iso and bootc-installer are all confusing. + generic-iso: &generic-iso <<: *raw_image_type mime_type: "application/x-iso9660-image" exports: ["bootiso"] @@ -202,6 +202,10 @@ image_types: blueprint: supported_options: *supported_options_iso + # XXX: ideally we would use name_aliases but the loader lib + # does not not fully support this yet + bootc-generic-iso: *generic-iso + # this image type is special (in many ways) and all is a bit ugly: # - we want to get rid of it (here for compat with bib) # - its "indirect" in the sense that it pulls the RPMs from a diff --git a/pkg/distro/generic/bootc_test.go b/pkg/distro/generic/bootc_test.go index e2abe2efee..f34dff0426 100644 --- a/pkg/distro/generic/bootc_test.go +++ b/pkg/distro/generic/bootc_test.go @@ -721,7 +721,7 @@ var diskContainers = map[string][]container.Spec{ }, } -// isoContainers can be passed to Serialize() to get a minimal bootc-generic-iso image +// isoContainers can be passed to Serialize() to get a minimal generic-iso image var isoContainers = map[string][]container.Spec{ "build": { containerSpec, @@ -973,7 +973,7 @@ func TestManifestGenerationBlueprintValidation(t *testing.T) { func TestBootcIsoManifestSerialization(t *testing.T) { bd := NewTestBootcDistro(t) - imgType, err := bd.arches["x86_64"].GetImageType("bootc-generic-iso") + imgType, err := bd.arches["x86_64"].GetImageType("generic-iso") assert.NoError(t, err) bp := &blueprint.Blueprint{} @@ -1015,7 +1015,7 @@ func TestContainerSourceLocality(t *testing.T) { switch imgTypeName { case "anaconda-iso", "iso": t.Skipf("skipping %s: legacy ISO requires real distro definitions not available in the test distro", imgTypeName) - case "bootc-generic-iso": + case "generic-iso", "bootc-generic-iso": t.Skipf("skipping %s: installer payload not supported ", imgTypeName) } diff --git a/test/bootc-fake-containers.yaml b/test/bootc-fake-containers.yaml index c113198273..b731cd35ef 100644 --- a/test/bootc-fake-containers.yaml +++ b/test/bootc-fake-containers.yaml @@ -93,15 +93,15 @@ containers: id: "fedora" versionid: "42" - # test bootc-generic-iso + # test generic-iso - <<: *default_fake_container - image_types: ["bootc-generic-iso"] + image_types: ["generic-iso"] arch: x86_64 info: <<: *default_info uefi_vendor: "uefi-vendor-x86_64" - <<: *default_fake_container - image_types: ["bootc-generic-iso"] + image_types: ["generic-iso"] arch: aarch64 info: <<: *default_info diff --git a/test/config-list.json b/test/config-list.json index 7bec3e62aa..8003b2f410 100644 --- a/test/config-list.json +++ b/test/config-list.json @@ -739,7 +739,7 @@ "vmdk", "ova", "anaconda-iso", - "bootc-generic-iso", + "generic-iso", "pxe-tar-xz" ] } @@ -770,7 +770,7 @@ "vmdk", "ova", "anaconda-iso", - "bootc-generic-iso", + "generic-iso", "pxe-tar-xz" ] } diff --git a/test/data/manifest-checksums/bootc_test_os_1-aarch64-bootc_generic_iso-bootc_empty b/test/data/manifest-checksums/bootc_test_os_1-aarch64-generic_iso-bootc_empty similarity index 100% rename from test/data/manifest-checksums/bootc_test_os_1-aarch64-bootc_generic_iso-bootc_empty rename to test/data/manifest-checksums/bootc_test_os_1-aarch64-generic_iso-bootc_empty diff --git a/test/data/manifest-checksums/bootc_test_os_1-aarch64-bootc_generic_iso-bootc_remote_empty b/test/data/manifest-checksums/bootc_test_os_1-aarch64-generic_iso-bootc_remote_empty similarity index 100% rename from test/data/manifest-checksums/bootc_test_os_1-aarch64-bootc_generic_iso-bootc_remote_empty rename to test/data/manifest-checksums/bootc_test_os_1-aarch64-generic_iso-bootc_remote_empty diff --git a/test/data/manifest-checksums/bootc_test_os_1-x86_64-bootc_generic_iso-bootc_empty b/test/data/manifest-checksums/bootc_test_os_1-x86_64-generic_iso-bootc_empty similarity index 100% rename from test/data/manifest-checksums/bootc_test_os_1-x86_64-bootc_generic_iso-bootc_empty rename to test/data/manifest-checksums/bootc_test_os_1-x86_64-generic_iso-bootc_empty diff --git a/test/data/manifest-checksums/bootc_test_os_1-x86_64-bootc_generic_iso-bootc_remote_empty b/test/data/manifest-checksums/bootc_test_os_1-x86_64-generic_iso-bootc_remote_empty similarity index 100% rename from test/data/manifest-checksums/bootc_test_os_1-x86_64-bootc_generic_iso-bootc_remote_empty rename to test/data/manifest-checksums/bootc_test_os_1-x86_64-generic_iso-bootc_remote_empty diff --git a/test/scripts/imgtestlib/boot.py b/test/scripts/imgtestlib/boot.py index 9663f0aa39..95d96029c7 100644 --- a/test/scripts/imgtestlib/boot.py +++ b/test/scripts/imgtestlib/boot.py @@ -624,7 +624,7 @@ def boot_image(search_path, build_config_path, keep_booted=False): boot_qemu(arch, image_path, build_config_path, keep_booted=keep_booted) elif image_type in ("image-installer", "minimal-installer"): boot_qemu_iso(arch, image_path, build_config_path) - elif image_type in ("network-installer", "everything-network-installer", "bootc-generic-iso"): + elif image_type in ("network-installer", "everything-network-installer", "generic-iso"): boot_qemu_iso_no_unattended_support(arch, image_path, build_config_path) elif image_type in ("pxe-tar-xz"): boot_qemu_pxe(arch, image_path) From afa426587409f9e70293127561eb19ec2e026800 Mon Sep 17 00:00:00 2001 From: "Brian C. Lane" Date: Fri, 12 Jun 2026 13:16:45 -0700 Subject: [PATCH 07/12] manifest-checksums: Add the bootc pxe-tar-xz image This adds generation of the manifest checksums. Related: HMS-10627 --- test/bootc-fake-containers.yaml | 14 ++++++++++++++ .../bootc_test_os_1-aarch64-pxe_tar_xz-bootc_empty | 1 + ...test_os_1-aarch64-pxe_tar_xz-bootc_remote_empty | 1 + .../bootc_test_os_1-x86_64-pxe_tar_xz-bootc_empty | 1 + ..._test_os_1-x86_64-pxe_tar_xz-bootc_remote_empty | 1 + 5 files changed, 18 insertions(+) create mode 100644 test/data/manifest-checksums/bootc_test_os_1-aarch64-pxe_tar_xz-bootc_empty create mode 100644 test/data/manifest-checksums/bootc_test_os_1-aarch64-pxe_tar_xz-bootc_remote_empty create mode 100644 test/data/manifest-checksums/bootc_test_os_1-x86_64-pxe_tar_xz-bootc_empty create mode 100644 test/data/manifest-checksums/bootc_test_os_1-x86_64-pxe_tar_xz-bootc_remote_empty diff --git a/test/bootc-fake-containers.yaml b/test/bootc-fake-containers.yaml index b731cd35ef..3619bc2a02 100644 --- a/test/bootc-fake-containers.yaml +++ b/test/bootc-fake-containers.yaml @@ -106,3 +106,17 @@ containers: info: <<: *default_info uefi_vendor: "uefi-vendor-aarch64" + + # test pxe-tar-xz + - <<: *default_fake_container + image_types: ["pxe-tar-xz"] + arch: x86_64 + info: + <<: *default_info + uefi_vendor: "uefi-vendor-x86_64" + - <<: *default_fake_container + image_types: ["pxe-tar-xz"] + arch: aarch64 + info: + <<: *default_info + uefi_vendor: "uefi-vendor-aarch64" diff --git a/test/data/manifest-checksums/bootc_test_os_1-aarch64-pxe_tar_xz-bootc_empty b/test/data/manifest-checksums/bootc_test_os_1-aarch64-pxe_tar_xz-bootc_empty new file mode 100644 index 0000000000..8f60a0d03b --- /dev/null +++ b/test/data/manifest-checksums/bootc_test_os_1-aarch64-pxe_tar_xz-bootc_empty @@ -0,0 +1 @@ +958192ffd7fb8872faf14384a1f1404987b2b0f5a493f67dac3e3cf1dd20e918 diff --git a/test/data/manifest-checksums/bootc_test_os_1-aarch64-pxe_tar_xz-bootc_remote_empty b/test/data/manifest-checksums/bootc_test_os_1-aarch64-pxe_tar_xz-bootc_remote_empty new file mode 100644 index 0000000000..d6076786b8 --- /dev/null +++ b/test/data/manifest-checksums/bootc_test_os_1-aarch64-pxe_tar_xz-bootc_remote_empty @@ -0,0 +1 @@ +2057d4290c1274b30ae327f53a58ef21546833236b75c0b1a30bc0ee58ed8556 diff --git a/test/data/manifest-checksums/bootc_test_os_1-x86_64-pxe_tar_xz-bootc_empty b/test/data/manifest-checksums/bootc_test_os_1-x86_64-pxe_tar_xz-bootc_empty new file mode 100644 index 0000000000..6db5696ffe --- /dev/null +++ b/test/data/manifest-checksums/bootc_test_os_1-x86_64-pxe_tar_xz-bootc_empty @@ -0,0 +1 @@ +3b9b6989897b2e4f6a3e61e8ea040645c13133f3200c01a5140e5e9368cf6d9c diff --git a/test/data/manifest-checksums/bootc_test_os_1-x86_64-pxe_tar_xz-bootc_remote_empty b/test/data/manifest-checksums/bootc_test_os_1-x86_64-pxe_tar_xz-bootc_remote_empty new file mode 100644 index 0000000000..1dd6a3a123 --- /dev/null +++ b/test/data/manifest-checksums/bootc_test_os_1-x86_64-pxe_tar_xz-bootc_remote_empty @@ -0,0 +1 @@ +f4cc98d245b18b74d8792b99210299f334fc9e629e36d63bea0219cc0485e1a3 From 95ea1884428715ea8892b3fb6dd2fdaafe800ce0 Mon Sep 17 00:00:00 2001 From: "Brian C. Lane" Date: Tue, 16 Jun 2026 14:04:00 -0700 Subject: [PATCH 08/12] iso_bootloaders: Add support for menu timeout Set the menu timeout if it is non-zero. 0 means use the stage default, so it is always safe to set it. Related: HMS-10627 --- pkg/manifest/iso_bootloaders.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/pkg/manifest/iso_bootloaders.go b/pkg/manifest/iso_bootloaders.go index 3b31851b2c..25e4d126bd 100644 --- a/pkg/manifest/iso_bootloaders.go +++ b/pkg/manifest/iso_bootloaders.go @@ -63,6 +63,8 @@ type Grub2X86Boot struct { // Default Grub2 menu on the ISO DefaultMenu int + // Grub2 menu timeout (0 == default) + MenuTimeout int // Custom menu entries (overrides all default entries) Custom []ISOGrub2MenuEntry } @@ -81,9 +83,10 @@ func (boot *Grub2X86Boot) GetISOBootStages(inputName string, _ *disk.PartitionTa stages := make([]*osbuild.Stage, 0) var grub2config *osbuild.Grub2Config - if boot.DefaultMenu > 0 { + if boot.DefaultMenu > 0 || boot.MenuTimeout > 0 { grub2config = &osbuild.Grub2Config{ Default: boot.DefaultMenu, + Timeout: boot.MenuTimeout, } } options := &osbuild.Grub2ISOLegacyStageOptions{ @@ -142,6 +145,8 @@ type Grub2PPC64Boot struct { // Default Grub2 menu on the ISO DefaultMenu int + // Grub2 menu timeout (0 == default) + MenuTimeout int // Custom menu entries (overrides all default entries) Custom []ISOGrub2MenuEntry } @@ -160,9 +165,10 @@ func (boot *Grub2PPC64Boot) GetISOBootStages(inputName string, _ *disk.Partition stages := make([]*osbuild.Stage, 0) var grub2config *osbuild.Grub2Config - if boot.DefaultMenu > 0 { + if boot.DefaultMenu > 0 || boot.MenuTimeout > 0 { grub2config = &osbuild.Grub2Config{ Default: boot.DefaultMenu, + Timeout: boot.MenuTimeout, } } options := &osbuild.Grub2ISOLegacyStageOptions{ From aa95833d0903df250a4d4de5b5037ceb86f2e887 Mon Sep 17 00:00:00 2001 From: "Brian C. Lane" Date: Tue, 16 Jun 2026 14:31:48 -0700 Subject: [PATCH 09/12] manifest: Add MenuTimeout to InstallerCustomizations Related: HMS-10627 --- pkg/manifest/installer.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/manifest/installer.go b/pkg/manifest/installer.go index 152f33362a..e1108b169e 100644 --- a/pkg/manifest/installer.go +++ b/pkg/manifest/installer.go @@ -134,7 +134,10 @@ type InstallerCustomizations struct { // Install weak dependencies in the installer environment InstallWeakDeps bool + // Grub2 default menu DefaultMenu int + // Grub2 menu timeout (0 == default) + MenuTimeout int Product string Variant string From 613e843b84d6c7da41ad7868edbfaa31cd83bd0f Mon Sep 17 00:00:00 2001 From: "Brian C. Lane" Date: Tue, 16 Jun 2026 14:32:41 -0700 Subject: [PATCH 10/12] iso_bootloaders: Set MenuTimeout from InstallerCustomizations Also adds tests. Related: HMS-10627 --- pkg/image/iso_bootloaders.go | 5 ++ pkg/image/iso_bootloaders_test.go | 90 +++++++++++++++++++++++-------- 2 files changed, 72 insertions(+), 23 deletions(-) diff --git a/pkg/image/iso_bootloaders.go b/pkg/image/iso_bootloaders.go index e65ae4f39e..78ca6b2c91 100644 --- a/pkg/image/iso_bootloaders.go +++ b/pkg/image/iso_bootloaders.go @@ -37,6 +37,7 @@ func (img *ISOBootloaders) Bootloaders(buildPipeline manifest.Build, platform pl grub2.ISOLabel = img.ISOCustomizations.Label grub2.KernelOpts = kernelOpts grub2.DefaultMenu = img.InstallerCustomizations.DefaultMenu + grub2.MenuTimeout = img.InstallerCustomizations.MenuTimeout for _, entry := range img.Custom { grub2.Custom = append(grub2.Custom, manifest.ISOGrub2MenuEntry{ @@ -55,6 +56,7 @@ func (img *ISOBootloaders) Bootloaders(buildPipeline manifest.Build, platform pl grub2ppc64.ISOLabel = img.ISOCustomizations.Label grub2ppc64.KernelOpts = kernelOpts grub2ppc64.DefaultMenu = img.InstallerCustomizations.DefaultMenu + grub2ppc64.MenuTimeout = img.InstallerCustomizations.MenuTimeout for _, entry := range img.Custom { grub2ppc64.Custom = append(grub2ppc64.Custom, manifest.ISOGrub2MenuEntry{ @@ -80,6 +82,9 @@ func (img *ISOBootloaders) Bootloaders(buildPipeline manifest.Build, platform pl efiPipeline.UEFIVendor = platform.GetUEFIVendor() efiPipeline.ISOLabel = img.ISOCustomizations.Label efiPipeline.DefaultMenu = img.InstallerCustomizations.DefaultMenu + if img.InstallerCustomizations.MenuTimeout > 0 { + efiPipeline.MenuTimeout = &img.InstallerCustomizations.MenuTimeout + } efiPipeline.KernelOpts = kernelOpts for _, entry := range img.Custom { diff --git a/pkg/image/iso_bootloaders_test.go b/pkg/image/iso_bootloaders_test.go index 6f6d571270..1a290ed9c0 100644 --- a/pkg/image/iso_bootloaders_test.go +++ b/pkg/image/iso_bootloaders_test.go @@ -5,22 +5,19 @@ import ( "math/rand" "testing" + "github.com/osbuild/image-builder/internal/common" "github.com/osbuild/image-builder/pkg/customizations/fsnode" "github.com/osbuild/image-builder/pkg/disk" "github.com/osbuild/image-builder/pkg/image" "github.com/osbuild/image-builder/pkg/manifest" "github.com/osbuild/image-builder/pkg/osbuild" "github.com/osbuild/image-builder/pkg/runner" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestBootType(t *testing.T) { - ibl := image.ISOBootloaders{ - InstallerCustomizations: &manifest.InstallerCustomizations{Product: "Fedora", OSVersion: "44"}, - ISOCustomizations: &manifest.ISOCustomizations{Label: "Fedora-44-test"}, - } - source := rand.NewSource(int64(0)) // math/rand is good enough in this case /* #nosec G404 */ @@ -35,9 +32,11 @@ func TestBootType(t *testing.T) { } type testCase struct { - bootType manifest.ISOBootType - custom []manifest.ISOGrub2MenuEntry - expected []results + bootType manifest.ISOBootType + custom []manifest.ISOGrub2MenuEntry + defaultMenu *int + menuTimeout *int + expected []results } var noCustomMenus []manifest.ISOGrub2MenuEntry @@ -52,24 +51,38 @@ func TestBootType(t *testing.T) { // Check the stages and files for each bootloader type tests := []testCase{ - testCase{manifest.Grub2UEFIOnlyISOBoot, noCustomMenus, []results{{ - stages: []string{ - "org.osbuild.truncate", - "org.osbuild.mkfs.fat", - "org.osbuild.copy", - "org.osbuild.copy"}, - paths: []string{}}, - }}, - testCase{manifest.SyslinuxISOBoot, noCustomMenus, []results{ - {stages: []string{"org.osbuild.isolinux"}, paths: []string{}}, { + testCase{manifest.Grub2UEFIOnlyISOBoot, noCustomMenus, nil, nil, + []results{{ stages: []string{ "org.osbuild.truncate", "org.osbuild.mkfs.fat", "org.osbuild.copy", "org.osbuild.copy"}, paths: []string{}}, + }}, + testCase{manifest.SyslinuxISOBoot, noCustomMenus, nil, nil, + []results{ + {stages: []string{"org.osbuild.isolinux"}, paths: []string{}}, { + stages: []string{ + "org.osbuild.truncate", + "org.osbuild.mkfs.fat", + "org.osbuild.copy", + "org.osbuild.copy"}, + paths: []string{}}, + }}, + testCase{manifest.Grub2ISOBoot, noCustomMenus, nil, nil, []results{{ + stages: []string{ + "org.osbuild.grub2.iso.legacy", + "org.osbuild.grub2.inst"}, + paths: []string{}}, { + stages: []string{ + "org.osbuild.truncate", + "org.osbuild.mkfs.fat", + "org.osbuild.copy", + "org.osbuild.copy"}, + paths: []string{}}, }}, - testCase{manifest.Grub2ISOBoot, noCustomMenus, []results{{ + testCase{manifest.Grub2ISOBoot, customMenus, nil, nil, []results{{ stages: []string{ "org.osbuild.grub2.iso.legacy", "org.osbuild.grub2.inst"}, @@ -81,7 +94,7 @@ func TestBootType(t *testing.T) { "org.osbuild.copy"}, paths: []string{}}, }}, - testCase{manifest.Grub2ISOBoot, customMenus, []results{{ + testCase{manifest.Grub2ISOBoot, customMenus, common.ToPtr(1), common.ToPtr(5), []results{{ stages: []string{ "org.osbuild.grub2.iso.legacy", "org.osbuild.grub2.inst"}, @@ -93,21 +106,29 @@ func TestBootType(t *testing.T) { "org.osbuild.copy"}, paths: []string{}}, }}, - testCase{manifest.Grub2PPCISOBoot, noCustomMenus, []results{{ + testCase{manifest.Grub2PPCISOBoot, noCustomMenus, nil, nil, + []results{{ + stages: []string{ + "org.osbuild.grub2.iso.legacy", + "org.osbuild.mkdir", + "org.osbuild.copy"}, + paths: []string{"/ppc/bootinfo.txt"}}, + }}, + testCase{manifest.Grub2PPCISOBoot, customMenus, nil, nil, []results{{ stages: []string{ "org.osbuild.grub2.iso.legacy", "org.osbuild.mkdir", "org.osbuild.copy"}, paths: []string{"/ppc/bootinfo.txt"}}, }}, - testCase{manifest.Grub2PPCISOBoot, customMenus, []results{{ + testCase{manifest.Grub2PPCISOBoot, customMenus, common.ToPtr(3), common.ToPtr(15), []results{{ stages: []string{ "org.osbuild.grub2.iso.legacy", "org.osbuild.mkdir", "org.osbuild.copy"}, paths: []string{"/ppc/bootinfo.txt"}}, }}, - testCase{manifest.S390ISOBoot, noCustomMenus, []results{{ + testCase{manifest.S390ISOBoot, noCustomMenus, nil, nil, []results{{ stages: []string{ "org.osbuild.copy", "org.osbuild.copy", @@ -128,8 +149,19 @@ func TestBootType(t *testing.T) { for _, tc := range tests { mf := manifest.New() buildPipeline := image.AddBuildBootstrapPipelines(&mf, runner, nil, nil) + + ibl := image.ISOBootloaders{ + InstallerCustomizations: &manifest.InstallerCustomizations{Product: "Fedora", OSVersion: "44"}, + ISOCustomizations: &manifest.ISOCustomizations{Label: "Fedora-44-test"}, + } ibl.ISOCustomizations.BootType = tc.bootType ibl.Custom = tc.custom + if tc.defaultMenu != nil { + ibl.InstallerCustomizations.DefaultMenu = *tc.defaultMenu + } + if tc.menuTimeout != nil { + ibl.InstallerCustomizations.MenuTimeout = *tc.menuTimeout + } bootloaders := ibl.Bootloaders(buildPipeline, testPlatform, []string{}) require.Len(t, bootloaders, len(tc.expected)) @@ -149,6 +181,18 @@ func TestBootType(t *testing.T) { options := grub2Stage.Options.(*osbuild.Grub2ISOLegacyStageOptions) require.NotNil(t, options) checkCustomMenus(t, tc.custom, options.Custom) + if tc.defaultMenu != nil { + require.NotNil(t, options.Config) + assert.Equal(t, *tc.defaultMenu, options.Config.Default) + } else { + assert.Nil(t, options.Config) + } + if tc.menuTimeout != nil { + require.NotNil(t, options.Config) + assert.Equal(t, *tc.menuTimeout, options.Config.Timeout) + } else { + assert.Nil(t, options.Config) + } } } } From f1c5c67f962438a6ddd57bedb841db745ca1d38b Mon Sep 17 00:00:00 2001 From: "Brian C. Lane" Date: Tue, 16 Jun 2026 14:33:44 -0700 Subject: [PATCH 11/12] bootc_imagetype: Use menu timeout from iso.yaml Related: HMS-10627 --- pkg/distro/generic/bootc_imagetype.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pkg/distro/generic/bootc_imagetype.go b/pkg/distro/generic/bootc_imagetype.go index 1563d93d1c..c1a7234b93 100644 --- a/pkg/distro/generic/bootc_imagetype.go +++ b/pkg/distro/generic/bootc_imagetype.go @@ -564,8 +564,10 @@ func (t *bootcImageType) manifestForGenericISO(bp *blueprint.Blueprint, options // TODO This really belongs in ISOCustomization img.InstallerCustomizations.DefaultMenu = *isoi.Grub2.Default } - // TODO -- add timeout to one of the customizations - // img.Grub2MenuTimeout = isoi.Grub2.Timeout + if isoi.Grub2.Timeout != nil { + // TODO This really belongs in ISOCustomization + img.InstallerCustomizations.MenuTimeout = *isoi.Grub2.Timeout + } if len(isoi.KernelArgs) > 0 { img.KernelOpts = isoi.KernelArgs From 85c11ccd44946f9bc52fec2a46051ce378068e6a Mon Sep 17 00:00:00 2001 From: "Brian C. Lane" Date: Tue, 30 Jun 2026 14:24:22 -0700 Subject: [PATCH 12/12] doc: Update the documentation for the generic-iso Related: HMS-10627 --- doc/20-advanced/20-bootc/10-isos.md | 223 +++++++++++++++------------- 1 file changed, 120 insertions(+), 103 deletions(-) diff --git a/doc/20-advanced/20-bootc/10-isos.md b/doc/20-advanced/20-bootc/10-isos.md index c60142e807..4fe68571d9 100644 --- a/doc/20-advanced/20-bootc/10-isos.md +++ b/doc/20-advanced/20-bootc/10-isos.md @@ -1,132 +1,149 @@ -# ISOs +# bootc generic-iso -## Generic +This ISO contains a bootable bootc root filesystem in /LiveOS/squashfs.img, the ISO +is bootable as an ISO or as an image written to a USB flash drive. -`image-builder` can build ISOs out of bootable containers. The image type to use to build ISOs is the `bootc-generic-iso` image type. `image-builder` takes the bootable container and explodes the relevant parts to put them into the correct places on the ISO this means that there is a small contract in place on what `image-builder` expects to exist in your bootable container: +This ISO is created from a bootc container using image-builder. You create a +custom container using podman, then run image-builder to turn it into an ISO. -1. A kernel must live in `/usr/lib/module/*/vmlinuz`. If there are multiple kernels the behavior is undefined. This kernel will be placed in `/images/pxeboot/vmlinuz` on the ISO filesystem. -2. An initramfs is expected to be next to the kernel with the filename `initramfs.img`. The initramfs is placed in `/images/pxeboot/initrd.img` on the ISO filesystem. -3. The UEFI vendor is sourced by a directory name in `/usr/lib/efi/shim/*EFI/$VENDOR`. If there are multiple directories the behavior is undefined. The `BOOT` directory is always ignored. -4. shim and grub2 EFI binaries (`shimx64.efi`, `mmx64.efi`, `gcdx64.efi`) are expected to be present in `/boot/efi/EFI/$VENDOR`. -5. `/usr/share/grub2/unicode.pf` and `/usr/lib/grub/i386-pc` are expected to present. These are normally provided by the `grub2-common` and `grub2-pc-modules` packages respectively. The latter is only necessary on `x86_64`. -5. Required executables in the container are: `podman`, `mksquashfs`, `xorriso`, `implantisomd5`, `grub2-mkimage` and `python`. If you are using a separate build container then these executables must exist in the build container. -6. The container image is converted to a `squashfs` filesystem and put into `/LiveOS/squashfs.img` in the ISO. +## bootc container -You can [define additional configuration](./05-sources-of-configuration.md#isoyaml) for an ISO inside your container. +The bootc container has a few requirements: -If a `--bootc-installer-payload-ref` argument is optionally passed to `image-builder` when building a `bootc-generic-iso` then the container reference is copied from the hosts container storage to `/var/lib/containers/storage` in the squashfs filesystem. +* Be based on a bootc container, eg. quay.io/fedora/fedora-bootc:latest +* Include the dracut-live, erofs-utils packages +* Include grub2 ISO bootloader related tools + - grub2-efi-*-cdboot xorriso isomd5sum shim +* Configure dracut to add the dmsquash-live module +* Configure ostree to not use composefs +* Rebuild the initramfs so that it includes the dmsquash-live module +* Optionally setup the ISO menus and kernel cmdline with iso.yaml -### Example Containerfile +## Sample Fedora bootc container -This container file builds a `Fedora` "payload" installer (a `boot.iso`). It installs the container that's mentioned in the `/usr/share/anaconda/interactive-defaults.ks` kickstart file. +This is a simple example using the `image-builder` cmdline tool and a local install of podman. -```Dockerfile -FROM quay.io/fedora/fedora-bootc:rawhide +Save this in `Containerfile`: -RUN dnf install -qy \ - anaconda \ - anaconda-install-img-deps \ - anaconda-dracut \ - dracut-config-generic \ - dracut-network \ - net-tools \ - grub2-efi-x64-cdboot \ - plymouth \ - default-fonts-core-sans \ - default-fonts-other-sans \ - google-noto-sans-cjk-fonts +``` +FROM quay.io/fedora/fedora-bootc:latest +RUN dnf -y install grub2-efi-*-cdboot xorriso isomd5sum dracut-live erofs-utils shim && dnf clean all +RUN mkdir /boot/efi && cp -r /usr/lib/efi/shim/*/EFI /boot/efi && cp -r /usr/lib/efi/grub2/*/EFI/* /boot/efi/EFI/ + +# Override using composefs for ostree (it is incompatible with the erofs rootfs) +RUN cat < /usr/lib/ostree/prepare-root.conf +[composefs] +enabled = no +[sysroot] +readonly = true +EOF + +# Include the dmsquash-live module in the initramfs +RUN cat < /usr/lib/dracut/dracut.conf.d/40-iso.conf +compress="xz" +add_dracutmodules+=" qemu qemu-net livenet dmsquash-live " +early_microcode="no" +EOF + +# Override the default ISO menus +RUN mkdir -p /usr/lib/image-builder/bootc +RUN cat < /usr/lib/image-builder/bootc/iso.yaml +label: bootc-generic +kernel_args: + - console=ttyS0 +grub2: + timeout: 5 + entries: + - name: Boot Linux + linux: \${kernelpath} \${root} + initrd: \${initrdpath} + - name: Boot Linux With debug + linux: \${kernelpath} \${root} rd.debug=1 + initrd: \${initrdpath} +EOF -# these are necessary build tools. if you use a separate build container then -# these tools should be installed there -RUN dnf install -qy \ - xorrisofs \ - squashfs-tools +# Rebuild the initrd +RUN set -xe; kver=$(ls /usr/lib/modules); env DRACUT_NO_XATTR=1 dracut -vf /usr/lib/modules/$kver/initramfs.img "$kver" -RUN dnf clean all +# Mask services that aren't compatible with running from an ISO +RUN systemctl mask bootc-generic-growpart.service bootc-publish-rhsm-facts.service bootloader-update.service rpm-ostree-fix-shadow-mode.service -RUN mkdir -p /boot/efi && cp -ra /usr/lib/efi/*/*/EFI /boot/efi +RUN bootc container lint +``` -# --- +Build this container using podman: +``` +podman build -f ./Containerfile -t bootc-iso +``` -# some configuration for our ISO +Run `image-builder` to create the ISO: +``` +image-builder build --bootc-default-fs ext4 --bootc-ref localhost/bootc-iso:latest generic-iso +``` -RUN mkdir -p /usr/lib/image-builder/bootc +If your container is on a remote system replace the +`localhost/bootc-iso:latest` with the right url. -COPY <> /etc/passwd && \ - echo "install::14438:0:99999:7:::" >> /etc/shadow && \ - passwd -d root - -RUN mv /usr/share/anaconda/list-harddrives-stub /usr/bin/list-harddrives && \ - mv /etc/yum.repos.d /etc/anaconda.repos.d && \ - ln -s /lib/systemd/system/anaconda.target /etc/systemd/system/default.target && \ - rm -v /usr/lib/systemd/system-generators/systemd-gpt-auto-generator - -RUN ln -s /usr/lib/systemd/system/anaconda-shell@.service /usr/lib/systemd/system/autovt@.service - -RUN mkdir /usr/lib/systemd/logind.conf.d -COPY < [!WARNING] -> *A `bootc`-system installed through Anaconda will fail to start the `systemd-remount-fs.service`. See [here](https://forge.fedoraproject.org/atomic-desktops/tracker/issues/72#issuecomment-593808) and [here](https://bugzilla.redhat.com/show_bug.cgi?id=2332319) for more information.* +Check the contents of `/usr/lib/ostree/prepare-root.conf` and +`/usr/lib/dracut/dracut.conf.d/40-iso.conf` to make sure they were created +correctly. You can also run `lsinitrd --mod /usr/lib/modules/*/initramfs.img` +to check to make sure the new initramfs contains the dmsquash-live and ostree +modules. +## Fails to mount the OSTree root -For more examples, including for other operating systems, you can take a look at [this demonstration repository](https://github.com/ondrejbudai/bootc-isos). +If you get an error like: -## Historical +ostree-prepare-root[848]: ostree-prepare-root: Couldn't find specified OSTree root -### `bootc-installer` +Check that the grub.cfg `ostree=...` entry in grub.cfg points to the path in the +rootfs.img. The build process sets this uuid from the ostree directory so this really should not happen with an ISO build unless you change the ISO contents yourself. -There's an alternative image type called `bootc-installer` which makes more assumptions about the contents of the container. You should prefer using `bootc-generic-iso`. +Or if the error looks like: -### `anaconda-iso` +ostree-prepare-root: Failed to mount composefs: composefs: failed to mount: Input/output error -There was an alternative image type called `anaconda-iso` or `iso` in `bootc-image-builder` but this image type is not available in `image-builder`. See the [migration guide](./50-migration.md). +Check the prepare-root.conf file to make sure composefs has been disabled.