From 4b9045d80d3bda50bd7a0205186e572aa0c69c00 Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Thu, 25 Sep 2025 14:36:34 +0200 Subject: [PATCH 1/5] image: rename AnacondaContainerInstaller->A..C..Legacy This commit renames the AnacondaContainerInstaller to AnacondaContainerInstallerLegacy. The reason is that we want to phase out this RPM based bootc container image. It is only used in bootc-image-builder to construct a boot ISO from the detected distros RPM packages. This (arguably) goes against the spirit of bootc where the container itself should be able to provide the functionality of the installer and not a bunch of RPMs. --- ... => anaconda_container_installer_legacy.go} | 8 ++++---- pkg/image/installer_image_test.go | 18 +++++++++--------- 2 files changed, 13 insertions(+), 13 deletions(-) rename pkg/image/{anaconda_container_installer.go => anaconda_container_installer_legacy.go} (93%) diff --git a/pkg/image/anaconda_container_installer.go b/pkg/image/anaconda_container_installer_legacy.go similarity index 93% rename from pkg/image/anaconda_container_installer.go rename to pkg/image/anaconda_container_installer_legacy.go index a7c933da77..041457ce8d 100644 --- a/pkg/image/anaconda_container_installer.go +++ b/pkg/image/anaconda_container_installer_legacy.go @@ -18,7 +18,7 @@ import ( "github.com/osbuild/images/pkg/runner" ) -type AnacondaContainerInstaller struct { +type AnacondaContainerInstallerLegacy struct { Base InstallerCustomizations manifest.InstallerCustomizations @@ -41,15 +41,15 @@ type AnacondaContainerInstaller struct { InstallRootfsType disk.FSType } -func NewAnacondaContainerInstaller(platform platform.Platform, filename string, container container.SourceSpec, ref string) *AnacondaContainerInstaller { - return &AnacondaContainerInstaller{ +func NewAnacondaContainerInstallerLegacy(platform platform.Platform, filename string, container container.SourceSpec, ref string) *AnacondaContainerInstallerLegacy { + return &AnacondaContainerInstallerLegacy{ Base: NewBase("container-installer", platform, filename), ContainerSource: container, Ref: ref, } } -func (img *AnacondaContainerInstaller) InstantiateManifest(m *manifest.Manifest, +func (img *AnacondaContainerInstallerLegacy) InstantiateManifest(m *manifest.Manifest, repos []rpmmd.RepoConfig, runner runner.Runner, rng *rand.Rand) (*artifact.Artifact, error) { diff --git a/pkg/image/installer_image_test.go b/pkg/image/installer_image_test.go index 167f25b94b..00a37e706d 100644 --- a/pkg/image/installer_image_test.go +++ b/pkg/image/installer_image_test.go @@ -99,7 +99,7 @@ const ( ) func TestContainerInstallerUnsetKSOptions(t *testing.T) { - img := image.NewAnacondaContainerInstaller(testPlatform, "filename", container.SourceSpec{}, "") + img := image.NewAnacondaContainerInstallerLegacy(testPlatform, "filename", container.SourceSpec{}, "") assert.NotNil(t, img) img.InstallerCustomizations.Product = product @@ -111,7 +111,7 @@ func TestContainerInstallerUnsetKSOptions(t *testing.T) { } func TestContainerInstallerUnsetKSPath(t *testing.T) { - img := image.NewAnacondaContainerInstaller(testPlatform, "filename", container.SourceSpec{}, "") + img := image.NewAnacondaContainerInstallerLegacy(testPlatform, "filename", container.SourceSpec{}, "") assert.NotNil(t, img) img.InstallerCustomizations.Product = product @@ -125,7 +125,7 @@ func TestContainerInstallerUnsetKSPath(t *testing.T) { } func TestContainerInstallerSetKSPath(t *testing.T) { - img := image.NewAnacondaContainerInstaller(testPlatform, "filename", container.SourceSpec{}, "") + img := image.NewAnacondaContainerInstallerLegacy(testPlatform, "filename", container.SourceSpec{}, "") assert.NotNil(t, img) img.InstallerCustomizations.Product = product @@ -141,7 +141,7 @@ func TestContainerInstallerSetKSPath(t *testing.T) { } func TestContainerInstallerExt4Rootfs(t *testing.T) { - img := image.NewAnacondaContainerInstaller(testPlatform, "filename", container.SourceSpec{}, "") + img := image.NewAnacondaContainerInstallerLegacy(testPlatform, "filename", container.SourceSpec{}, "") assert.NotNil(t, img) img.InstallerCustomizations.Product = product @@ -156,7 +156,7 @@ func TestContainerInstallerExt4Rootfs(t *testing.T) { } func TestContainerInstallerSquashfsRootfs(t *testing.T) { - img := image.NewAnacondaContainerInstaller(testPlatform, "filename", container.SourceSpec{}, "") + img := image.NewAnacondaContainerInstallerLegacy(testPlatform, "filename", container.SourceSpec{}, "") assert.NotNil(t, img) img.InstallerCustomizations.Product = product @@ -375,7 +375,7 @@ func instantiateAndSerialize(t *testing.T, img image.ImageKind, depsolved map[st func TestContainerInstallerPanics(t *testing.T) { assert := assert.New(t) - img := image.NewAnacondaContainerInstaller(testPlatform, "filename", container.SourceSpec{}, "") + img := image.NewAnacondaContainerInstallerLegacy(testPlatform, "filename", container.SourceSpec{}, "") assert.PanicsWithError("org.osbuild.grub2.iso: product.name option is required", func() { instantiateAndSerialize(t, img, mockPackageSets(), mockContainerSpecs(), nil) }) img.InstallerCustomizations.Product = product assert.PanicsWithError("org.osbuild.grub2.iso: product.version option is required", func() { instantiateAndSerialize(t, img, mockPackageSets(), mockContainerSpecs(), nil) }) @@ -486,7 +486,7 @@ func TestContainerInstallerModules(t *testing.T) { // Remove this when we drop support for RHEL 8. for _, legacy := range []bool{true, false} { t.Run(name, func(t *testing.T) { - img := image.NewAnacondaContainerInstaller(testPlatform, "filename", container.SourceSpec{}, "") + img := image.NewAnacondaContainerInstallerLegacy(testPlatform, "filename", container.SourceSpec{}, "") img.InstallerCustomizations.Product = product img.InstallerCustomizations.OSVersion = osversion img.InstallerCustomizations.ISOLabel = isolabel @@ -582,7 +582,7 @@ func TestInstallerLocales(t *testing.T) { for input, expected := range locales { { // Container - img := image.NewAnacondaContainerInstaller(testPlatform, "filename", container.SourceSpec{}, "") + img := image.NewAnacondaContainerInstallerLegacy(testPlatform, "filename", container.SourceSpec{}, "") assert.NotNil(t, img) img.InstallerCustomizations.Product = product @@ -713,7 +713,7 @@ func findGrub2IsoStageOptions(t *testing.T, mf manifest.OSBuildManifest, pipelin } func TestContainerInstallerDracut(t *testing.T) { - img := image.NewAnacondaContainerInstaller(testPlatform, "filename", container.SourceSpec{}, "") + img := image.NewAnacondaContainerInstallerLegacy(testPlatform, "filename", container.SourceSpec{}, "") img.InstallerCustomizations.Product = product img.InstallerCustomizations.OSVersion = osversion img.InstallerCustomizations.ISOLabel = isolabel From 5e1b2f8d5724a8b9bd3eb63871a6f076c5d78920 Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Thu, 25 Sep 2025 17:01:57 +0200 Subject: [PATCH 2/5] many: add infrastructure for bootc anaconda images This commit contains the infrastructure in the distro, image and manifest packages to support building bootc based ISO images. This includes: - suppport for setting an installer payload - a new bootc installer image type in pkg/distro/bootc - support for containers in image/anaconda_container_intaller - exported helper to share between bib/images until the legacy ISO rpm installer moved into images too (which is hard) In addition (this was originally a second commit but because the manifest checksum test run per-commit this test update needs to be all one commit): This commit adds reference manifests for the new bootc iso installer image type. It also tweaks `gen-manifests` a bit as we now need to split the tests between using disk and installer image types. --- cmd/gen-manifests/main.go | 27 ++- pkg/bib/osinfo/osinfo.go | 2 +- pkg/distro/bootc/bootc.go | 33 +-- pkg/distro/bootc/iso.go | 225 ++++++++++++++++++ pkg/distro/bootc/shared.go | 156 ++++++++++++ pkg/distro/bootc/shared_test.go | 58 +++++ pkg/image/anaconda_container_installer.go | 155 ++++++++++++ pkg/manifest/anaconda_installer.go | 83 ++++++- pkg/manifest/anaconda_installer_iso_tree.go | 16 +- test/bootc-fake-containers.yaml | 15 ++ ...tc_test_os_1-aarch64-bootc_installer-empty | 1 + ...otc_test_os_1-x86_64-bootc_installer-empty | 1 + 12 files changed, 736 insertions(+), 36 deletions(-) create mode 100644 pkg/distro/bootc/iso.go create mode 100644 pkg/distro/bootc/shared.go create mode 100644 pkg/distro/bootc/shared_test.go create mode 100644 pkg/image/anaconda_container_installer.go create mode 100644 test/data/manifest-checksums/bootc_test_os_1-aarch64-bootc_installer-empty create mode 100644 test/data/manifest-checksums/bootc_test_os_1-x86_64-bootc_installer-empty diff --git a/cmd/gen-manifests/main.go b/cmd/gen-manifests/main.go index c3cf112751..f3e9444637 100644 --- a/cmd/gen-manifests/main.go +++ b/cmd/gen-manifests/main.go @@ -14,6 +14,7 @@ import ( "os" "path/filepath" "runtime/debug" + "slices" "strings" "time" @@ -611,9 +612,11 @@ func main() { DefaultFs string `yaml:"default_fs"` ContainerSize uint64 `yaml:"container_size"` ImageRef string `yaml:"image_ref"` + ImageTypes []string `yaml:"image_types"` - BuildContainerRef string `yaml:"build_container_ref"` - BuildContainerInfo osinfo.Info `yaml:"build_container_info"` + BuildContainerRef string `yaml:"build_container_ref"` + BuildContainerInfo osinfo.Info `yaml:"build_container_info"` + PayloadContainerRef string `yaml:"payload_container_ref"` } type fakeContainersYAML struct { Containers []fakeBootcContainerYAML @@ -629,11 +632,6 @@ func main() { if err != nil { panic(err) } - if fakeBootcCnt.BuildContainerRef != "" { - if err := distribution.SetBuildContainerForTesting(fakeBootcCnt.BuildContainerRef, &fakeBootcCnt.BuildContainerInfo); err != nil { - panic(err) - } - } arches, _ := arches.ResolveArgValues(distribution.ListArches()) for _, archName := range arches { @@ -643,6 +641,21 @@ func main() { } imgTypes, _ := imgTypes.ResolveArgValues(archi.ListImageTypes()) for _, imgTypeName := range imgTypes { + if !slices.Contains(fakeBootcCnt.ImageTypes, imgTypeName) { + continue + } + + if fakeBootcCnt.BuildContainerRef != "" { + if err := distribution.SetBuildContainerForTesting(fakeBootcCnt.BuildContainerRef, &fakeBootcCnt.BuildContainerInfo); err != nil { + panic(err) + } + } + if fakeBootcCnt.PayloadContainerRef != "" { + if err := distribution.SetInstallerPayload(fakeBootcCnt.PayloadContainerRef); err != nil { + panic(err) + } + } + imgType, err := archi.GetImageType(imgTypeName) if err != nil { panic(err) diff --git a/pkg/bib/osinfo/osinfo.go b/pkg/bib/osinfo/osinfo.go index 693a24dd84..6dd4c1be33 100644 --- a/pkg/bib/osinfo/osinfo.go +++ b/pkg/bib/osinfo/osinfo.go @@ -40,7 +40,7 @@ type Info struct { UEFIVendor string `yaml:"uefi_vendor"` SELinuxPolicy string `yaml:"selinux_policy"` ImageCustomization *blueprint.Customizations - KernelInfo *KernelInfo + KernelInfo *KernelInfo `yaml:"kernel_info"` MountConfiguration *osbuild.MountConfiguration PartitionTable *disk.PartitionTable diff --git a/pkg/distro/bootc/bootc.go b/pkg/distro/bootc/bootc.go index f4101dad94..152b7cd915 100644 --- a/pkg/distro/bootc/bootc.go +++ b/pkg/distro/bootc/bootc.go @@ -31,8 +31,10 @@ import ( var _ = distro.Distro(&BootcDistro{}) type BootcDistro struct { - imgref string - buildImgref string + imgref string + buildImgref string + // XXX: wrong place? + payloadRef string sourceInfo *osinfo.Info buildSourceInfo *osinfo.Info @@ -83,6 +85,12 @@ func (d *BootcDistro) SetBuildContainer(imgref string) (err error) { return d.setBuildContainer(imgref, info) } +// XXX: wrong layer +func (d *BootcDistro) SetInstallerPayload(imgref string) error { + d.payloadRef = imgref + return nil +} + func (d *BootcDistro) setBuildContainer(imgref string, info *osinfo.Info) error { d.buildImgref = imgref d.buildSourceInfo = info @@ -312,20 +320,7 @@ func (t *BootcImageType) Manifest(bp *blueprint.Blueprint, options distro.ImageO //nolint:gosec rng := rand.New(rand.NewSource(seed)) - archi := common.Must(arch.FromString(t.arch.Name())) - platform := &platform.Data{ - Arch: archi, - UEFIVendor: t.arch.distro.sourceInfo.UEFIVendor, - QCOW2Compat: "1.1", - } - switch archi { - case arch.ARCH_X86_64: - platform.BIOSPlatform = "i386-pc" - case arch.ARCH_PPC64LE: - platform.BIOSPlatform = "powerpc-ieee1275" - case arch.ARCH_S390X: - platform.ZiplSupport = true - } + platform := PlatformFor(t.arch.Name(), t.arch.distro.sourceInfo.UEFIVendor) // For the bootc-disk image, the filename is the basename and // the extension is added automatically for each disk format filename := strings.Split(t.filename, ".")[0] @@ -487,6 +482,12 @@ func newBootcDistroAfterIntrospect(archStr string, info *osinfo.Info, imgref, de filename: "image.ova", }, ) + ba.imageTypes["bootc-installer"] = &BootcAnacondaInstaller{ + arch: ba, + name: "bootc-installer", + export: "bootiso", + } + bd.addArches(ba) return bd, nil diff --git a/pkg/distro/bootc/iso.go b/pkg/distro/bootc/iso.go new file mode 100644 index 0000000000..e73b12e63e --- /dev/null +++ b/pkg/distro/bootc/iso.go @@ -0,0 +1,225 @@ +package bootc + +import ( + "fmt" + "math/rand" + + "github.com/osbuild/blueprint/pkg/blueprint" + "github.com/osbuild/images/internal/cmdutil" + "github.com/osbuild/images/pkg/arch" + "github.com/osbuild/images/pkg/container" + "github.com/osbuild/images/pkg/customizations/anaconda" + "github.com/osbuild/images/pkg/customizations/kickstart" + "github.com/osbuild/images/pkg/disk" + "github.com/osbuild/images/pkg/distro" + "github.com/osbuild/images/pkg/image" + "github.com/osbuild/images/pkg/manifest" + "github.com/osbuild/images/pkg/osbuild" + "github.com/osbuild/images/pkg/platform" + "github.com/osbuild/images/pkg/rpmmd" +) + +var _ = distro.ImageType(&BootcAnacondaInstaller{}) + +// BootcAnacondaInstaller is an image-type for a bootc +// container based ISO installer. +type BootcAnacondaInstaller struct { + arch *BootcArch + + name string + export string +} + +func (t *BootcAnacondaInstaller) Name() string { + return t.name +} + +func (t *BootcAnacondaInstaller) Aliases() []string { + return nil +} + +func (t *BootcAnacondaInstaller) Arch() distro.Arch { + return t.arch +} + +func (t *BootcAnacondaInstaller) Filename() string { + return "installer.iso" +} + +func (t *BootcAnacondaInstaller) MIMEType() string { + return "application/x-iso9660-image" +} + +func (t *BootcAnacondaInstaller) OSTreeRef() string { + return "" +} + +func (t *BootcAnacondaInstaller) ISOLabel() (string, error) { + return "Unknown", nil +} + +func (t *BootcAnacondaInstaller) Size(size uint64) uint64 { + return size +} + +func (t *BootcAnacondaInstaller) PartitionType() disk.PartitionTableType { + return disk.PT_NONE +} + +func (t *BootcAnacondaInstaller) BasePartitionTable() (*disk.PartitionTable, error) { + return nil, nil +} + +func (t *BootcAnacondaInstaller) BootMode() platform.BootMode { + return platform.BOOT_HYBRID +} + +func (t *BootcAnacondaInstaller) BuildPipelines() []string { + return []string{"build"} +} + +func (t *BootcAnacondaInstaller) PayloadPipelines() []string { + return []string{""} +} + +func (t *BootcAnacondaInstaller) PayloadPackageSets() []string { + return nil +} + +func (t *BootcAnacondaInstaller) Exports() []string { + return []string{t.export} +} + +func (t *BootcAnacondaInstaller) SupportedBlueprintOptions() []string { + // XXX: this is probably too minimal but lets start small + // and expand + return []string{ + "customizations.fips", + "customizations.group", + "customizations.installer", + "customizations.kernel.append", + "customizations.user", + } +} +func (t *BootcAnacondaInstaller) RequiredBlueprintOptions() []string { + return nil +} + +// XXX: duplication with BootcImageType +func (t *BootcAnacondaInstaller) Manifest(bp *blueprint.Blueprint, options distro.ImageOptions, repos []rpmmd.RepoConfig, seedp *int64) (*manifest.Manifest, []string, error) { + if t.arch.distro.imgref == "" { + return nil, nil, fmt.Errorf("internal error: no base image defined") + } + containerSource := container.SourceSpec{ + Source: t.arch.distro.imgref, + Name: t.arch.distro.imgref, + Local: true, + } + // XXX: keep it simple for now, we may allow this in the future + if t.arch.distro.buildImgref != t.arch.distro.imgref { + return nil, nil, fmt.Errorf("cannot use build-containers with anaconda installer images") + } + + var customizations *blueprint.Customizations + if bp != nil { + customizations = bp.Customizations + } + seed, err := cmdutil.SeedArgFor(nil, t.Name(), t.arch.Name(), t.arch.distro.Name()) + if err != nil { + return nil, nil, err + } + //nolint:gosec + rng := rand.New(rand.NewSource(seed)) + + platformi := PlatformFor(t.arch.Name(), t.arch.distro.sourceInfo.UEFIVendor) + platformi.ImageFormat = platform.FORMAT_ISO + + // XXX: tons of copied code from + // bootc-image-builder:‎bib/cmd/bootc-image-builder/legacy_iso.go + // but sharing is hard because AnacondaContainerInstaller and + // AnacondaContainerInstallerLegacy are different types so + // a shared helper to set the fields won't work (unless + // reflection urgh). + filename := "install.iso" + + // The ref is not needed and will be removed from the ctor later + // in time + img := image.NewAnacondaContainerInstaller(platformi, filename, containerSource, "") + img.ContainerRemoveSignatures = true + img.RootfsCompression = "zstd" + // kernelVer is used by dracut + img.KernelVer = t.arch.distro.sourceInfo.KernelInfo.Version + img.KernelPath = fmt.Sprintf("lib/modules/%s/vmlinuz", t.arch.distro.sourceInfo.KernelInfo.Version) + img.InitramfsPath = fmt.Sprintf("lib/modules/%s/initramfs.img", t.arch.distro.sourceInfo.KernelInfo.Version) + img.InstallerHome = "/var/roothome" + payloadSource := container.SourceSpec{ + Source: t.arch.distro.payloadRef, + Name: t.arch.distro.payloadRef, + Local: true, + } + img.InstallerPayload = payloadSource + + if t.arch.Name() == arch.ARCH_X86_64.String() { + img.InstallerCustomizations.ISOBoot = manifest.Grub2ISOBoot + } + + img.InstallerCustomizations.Product = t.arch.distro.sourceInfo.OSRelease.Name + img.InstallerCustomizations.OSVersion = t.arch.distro.sourceInfo.OSRelease.VersionID + img.InstallerCustomizations.ISOLabel = LabelForISO(&t.arch.distro.sourceInfo.OSRelease, t.arch.Name()) + + img.InstallerCustomizations.FIPS = customizations.GetFIPS() + img.Kickstart, err = kickstart.New(customizations) + if err != nil { + return nil, nil, err + } + img.Kickstart.Path = osbuild.KickstartPathOSBuild + if kopts := customizations.GetKernel(); kopts != nil && kopts.Append != "" { + img.Kickstart.KernelOptionsAppend = append(img.Kickstart.KernelOptionsAppend, kopts.Append) + } + img.Kickstart.NetworkOnBoot = true + + instCust, err := customizations.GetInstaller() + if err != nil { + return nil, nil, err + } + if instCust != nil && instCust.Modules != nil { + img.InstallerCustomizations.EnabledAnacondaModules = append(img.InstallerCustomizations.EnabledAnacondaModules, instCust.Modules.Enable...) + img.InstallerCustomizations.DisabledAnacondaModules = append(img.InstallerCustomizations.DisabledAnacondaModules, instCust.Modules.Disable...) + } + img.InstallerCustomizations.EnabledAnacondaModules = append(img.InstallerCustomizations.EnabledAnacondaModules, + anaconda.ModuleUsers, + anaconda.ModuleServices, + anaconda.ModuleSecurity, + // XXX: get from the imagedefs + anaconda.ModuleNetwork, + anaconda.ModulePayloads, + anaconda.ModuleRuntime, + anaconda.ModuleStorage, + ) + + img.Kickstart.OSTree = &kickstart.OSTree{ + OSName: "default", + } + img.InstallerCustomizations.LoraxTemplates = LoraxTemplates(t.arch.distro.sourceInfo.OSRelease) + img.InstallerCustomizations.LoraxTemplatePackage = LoraxTemplatePackage(t.arch.distro.sourceInfo.OSRelease) + + // see https://github.com/osbuild/bootc-image-builder/issues/733 + img.InstallerCustomizations.ISORootfsType = manifest.SquashfsRootfs + + installRootfsType, err := disk.NewFSType(t.arch.distro.defaultFs) + if err != nil { + return nil, nil, err + } + img.InstallRootfsType = installRootfsType + + mf := manifest.New() + + foundDistro, foundRunner, err := GetDistroAndRunner(t.arch.distro.sourceInfo.OSRelease) + if err != nil { + return nil, nil, fmt.Errorf("failed to infer distro and runner: %w", err) + } + mf.Distro = foundDistro + + _, err = img.InstantiateManifestFromContainer(&mf, []container.SourceSpec{containerSource}, foundRunner, rng) + return &mf, nil, err +} diff --git a/pkg/distro/bootc/shared.go b/pkg/distro/bootc/shared.go new file mode 100644 index 0000000000..560476ca53 --- /dev/null +++ b/pkg/distro/bootc/shared.go @@ -0,0 +1,156 @@ +package bootc + +import ( + "fmt" + "slices" + "strconv" + "strings" + + "github.com/osbuild/images/internal/common" + "github.com/osbuild/images/pkg/arch" + "github.com/osbuild/images/pkg/bib/osinfo" + "github.com/osbuild/images/pkg/manifest" + "github.com/osbuild/images/pkg/olog" + "github.com/osbuild/images/pkg/platform" + "github.com/osbuild/images/pkg/runner" +) + +// This file contains shared helpers between the various bootc +// image types, both here and in bootc-image-builder. +// Once the legacy bootc ISO type has moved into images we can +// unexport most (all?) of these helpers. + +// TODO: find a way to move them into YAML to make sharing easier +// between package and image based image types +// +// from:https://github.com/osbuild/images/blob/v0.207.0/data/distrodefs/rhel-10/imagetypes.yaml#L169 +var loraxRhelTemplates = []manifest.InstallerLoraxTemplate{ + manifest.InstallerLoraxTemplate{Path: "80-rhel/runtime-postinstall.tmpl"}, + manifest.InstallerLoraxTemplate{Path: "80-rhel/runtime-cleanup.tmpl", AfterDracut: true}, +} + +// from:https://github.com/osbuild/images/blob/v0.207.0/data/distrodefs/fedora/imagetypes.yaml#L408 +var loraxFedoraTemplates = []manifest.InstallerLoraxTemplate{ + manifest.InstallerLoraxTemplate{Path: "99-generic/runtime-postinstall.tmpl"}, + manifest.InstallerLoraxTemplate{Path: "99-generic/runtime-cleanup.tmpl", AfterDracut: true}, +} + +// This will be reused by bootc-image-builder + +func LoraxTemplates(si osinfo.OSRelease) []manifest.InstallerLoraxTemplate { + switch { + case si.ID == "rhel" || slices.Contains(si.IDLike, "rhel") || si.VersionID == "eln": + return loraxRhelTemplates + default: + return loraxFedoraTemplates + } +} +func LoraxTemplatePackage(si osinfo.OSRelease) string { + switch { + case si.ID == "rhel" || slices.Contains(si.IDLike, "rhel") || si.VersionID == "eln": + return "lorax-templates-rhel" + default: + return "lorax-templates-generic" + } +} + +func PlatformFor(archStr, uefiVendor string) *platform.Data { + archi := common.Must(arch.FromString(archStr)) + platform := &platform.Data{ + Arch: archi, + UEFIVendor: uefiVendor, + QCOW2Compat: "1.1", + } + switch archi { + case arch.ARCH_X86_64: + platform.BIOSPlatform = "i386-pc" + case arch.ARCH_PPC64LE: + platform.BIOSPlatform = "powerpc-ieee1275" + case arch.ARCH_S390X: + platform.ZiplSupport = true + } + return platform +} + +func GetDistroAndRunner(osRelease osinfo.OSRelease) (manifest.Distro, runner.Runner, error) { + switch osRelease.ID { + case "fedora": + version, err := strconv.ParseUint(osRelease.VersionID, 10, 64) + if err != nil { + return manifest.DISTRO_NULL, nil, fmt.Errorf("cannot parse Fedora version (%s): %w", osRelease.VersionID, err) + } + + return manifest.DISTRO_FEDORA, &runner.Fedora{ + Version: version, + }, nil + case "centos": + version, err := strconv.ParseUint(osRelease.VersionID, 10, 64) + if err != nil { + return manifest.DISTRO_NULL, nil, fmt.Errorf("cannot parse CentOS version (%s): %w", osRelease.VersionID, err) + } + r := &runner.CentOS{ + Version: version, + } + switch version { + case 9: + return manifest.DISTRO_EL9, r, nil + case 10: + return manifest.DISTRO_EL10, r, nil + default: + olog.Printf("Unknown CentOS version %d, using default distro for manifest generation", version) + return manifest.DISTRO_NULL, r, nil + } + + case "rhel": + versionParts := strings.Split(osRelease.VersionID, ".") + if len(versionParts) != 2 { + return manifest.DISTRO_NULL, nil, fmt.Errorf("invalid RHEL version format: %s", osRelease.VersionID) + } + major, err := strconv.ParseUint(versionParts[0], 10, 64) + if err != nil { + return manifest.DISTRO_NULL, nil, fmt.Errorf("cannot parse RHEL major version (%s): %w", versionParts[0], err) + } + minor, err := strconv.ParseUint(versionParts[1], 10, 64) + if err != nil { + return manifest.DISTRO_NULL, nil, fmt.Errorf("cannot parse RHEL minor version (%s): %w", versionParts[1], err) + } + r := &runner.RHEL{ + Major: major, + Minor: minor, + } + switch major { + case 9: + return manifest.DISTRO_EL9, r, nil + case 10: + return manifest.DISTRO_EL10, r, nil + default: + olog.Printf("Unknown RHEL version %d, using default distro for manifest generation", major) + return manifest.DISTRO_NULL, r, nil + } + } + + olog.Printf("Unknown distro %s, using default runner", osRelease.ID) + return manifest.DISTRO_NULL, &runner.Linux{}, nil +} + +func NeedsRHELLoraxTemplates(si osinfo.OSRelease) bool { + return si.ID == "rhel" || slices.Contains(si.IDLike, "rhel") || si.VersionID == "eln" +} + +func LabelForISO(os *osinfo.OSRelease, arch string) string { + switch os.ID { + case "fedora": + return fmt.Sprintf("Fedora-S-dvd-%s-%s", arch, os.VersionID) + case "centos": + labelTemplate := "CentOS-Stream-%s-BaseOS-%s" + if os.VersionID == "8" { + labelTemplate = "CentOS-Stream-%s-%s-dvd" + } + return fmt.Sprintf(labelTemplate, os.VersionID, arch) + case "rhel": + version := strings.ReplaceAll(os.VersionID, ".", "-") + return fmt.Sprintf("RHEL-%s-BaseOS-%s", version, arch) + default: + return fmt.Sprintf("Container-Installer-%s", arch) + } +} diff --git a/pkg/distro/bootc/shared_test.go b/pkg/distro/bootc/shared_test.go new file mode 100644 index 0000000000..311dda0d72 --- /dev/null +++ b/pkg/distro/bootc/shared_test.go @@ -0,0 +1,58 @@ +package bootc_test + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/osbuild/images/pkg/manifest" + "github.com/osbuild/images/pkg/runner" + + "github.com/osbuild/images/pkg/bib/osinfo" + "github.com/osbuild/images/pkg/distro/bootc" +) + +func TestGetDistroAndRunner(t *testing.T) { + cases := []struct { + id string + versionID string + expectedDistro manifest.Distro + expectedRunner runner.Runner + expectedErr string + }{ + // Happy + {"fedora", "40", manifest.DISTRO_FEDORA, &runner.Fedora{Version: 40}, ""}, + {"centos", "9", manifest.DISTRO_EL9, &runner.CentOS{Version: 9}, ""}, + {"centos", "10", manifest.DISTRO_EL10, &runner.CentOS{Version: 10}, ""}, + {"centos", "11", manifest.DISTRO_NULL, &runner.CentOS{Version: 11}, ""}, + {"rhel", "9.4", manifest.DISTRO_EL9, &runner.RHEL{Major: 9, Minor: 4}, ""}, + {"rhel", "10.4", manifest.DISTRO_EL10, &runner.RHEL{Major: 10, Minor: 4}, ""}, + {"rhel", "11.4", manifest.DISTRO_NULL, &runner.RHEL{Major: 11, Minor: 4}, ""}, + {"toucanos", "42", manifest.DISTRO_NULL, &runner.Linux{}, ""}, + + // Sad + {"fedora", "asdf", manifest.DISTRO_NULL, nil, "cannot parse Fedora version (asdf)"}, + {"centos", "asdf", manifest.DISTRO_NULL, nil, "cannot parse CentOS version (asdf)"}, + {"rhel", "10", manifest.DISTRO_NULL, nil, "invalid RHEL version format: 10"}, + {"rhel", "10.asdf", manifest.DISTRO_NULL, nil, "cannot parse RHEL minor version (asdf)"}, + } + + for _, c := range cases { + t.Run(fmt.Sprintf("%s-%s", c.id, c.versionID), func(t *testing.T) { + osRelease := osinfo.OSRelease{ + ID: c.id, + VersionID: c.versionID, + } + distro, runner, err := bootc.GetDistroAndRunner(osRelease) + if c.expectedErr != "" { + assert.ErrorContains(t, err, c.expectedErr) + } else { + require.NoError(t, err) + assert.Equal(t, c.expectedDistro, distro) + assert.Equal(t, c.expectedRunner, runner) + } + }) + } +} diff --git a/pkg/image/anaconda_container_installer.go b/pkg/image/anaconda_container_installer.go new file mode 100644 index 0000000000..002b700124 --- /dev/null +++ b/pkg/image/anaconda_container_installer.go @@ -0,0 +1,155 @@ +package image + +import ( + "fmt" + "math/rand" + + "github.com/osbuild/images/pkg/arch" + "github.com/osbuild/images/pkg/artifact" + "github.com/osbuild/images/pkg/container" + "github.com/osbuild/images/pkg/customizations/anaconda" + "github.com/osbuild/images/pkg/customizations/kickstart" + "github.com/osbuild/images/pkg/datasizes" + "github.com/osbuild/images/pkg/disk" + "github.com/osbuild/images/pkg/manifest" + "github.com/osbuild/images/pkg/osbuild" + "github.com/osbuild/images/pkg/platform" + "github.com/osbuild/images/pkg/runner" +) + +type AnacondaContainerInstaller struct { + Base + + InstallerCustomizations manifest.InstallerCustomizations + + RootfsCompression string + + Ref string + + ContainerSource container.SourceSpec + InstallerPayload container.SourceSpec + ContainerRemoveSignatures bool + + Kickstart *kickstart.Options + + // Locale for the installer. This should be set to the same locale as the + // ISO OS payload, if known. + Locale string + + // Filesystem type for the installed system as opposed to that of the ISO. + InstallRootfsType disk.FSType + + // KernelVer is needed so that dracut finds it files + KernelVer string + // {Kernel,Initramfs}Path is needed for grub2.iso + KernelPath string + InitramfsPath string + // bootc installer cannot use /root as installer home + InstallerHome string +} + +func NewAnacondaContainerInstaller(platform platform.Platform, filename string, container container.SourceSpec, ref string) *AnacondaContainerInstaller { + return &AnacondaContainerInstaller{ + Base: NewBase("bootc-installer", platform, filename), + ContainerSource: container, + Ref: ref, + } +} + +func (img *AnacondaContainerInstaller) InstantiateManifestFromContainer(m *manifest.Manifest, + containers []container.SourceSpec, + runner runner.Runner, + rng *rand.Rand) (*artifact.Artifact, error) { + cnts := []container.SourceSpec{img.ContainerSource} + buildPipeline := manifest.NewBuildFromContainer(m, runner, cnts, + &manifest.BuildOptions{ + ContainerBuildable: true, + }) + + anacondaPipeline := manifest.NewAnacondaInstaller( + manifest.AnacondaInstallerTypePayload, + buildPipeline, + img.platform, + nil, // repos + "kernel", + img.InstallerCustomizations, + ) + // with bootc we need different kernel/initramfs paths + anacondaPipeline.BootcLivefsContainer = &img.ContainerSource + anacondaPipeline.KernelPath = img.KernelPath + anacondaPipeline.InitramfsPath = img.InitramfsPath + anacondaPipeline.SetKernelVer(img.KernelVer) + anacondaPipeline.InstallerHome = img.InstallerHome + + anacondaPipeline.Biosdevname = (img.platform.GetArch() == arch.ARCH_X86_64) + anacondaPipeline.Checkpoint() + + if anacondaPipeline.InstallerCustomizations.FIPS { + anacondaPipeline.InstallerCustomizations.EnabledAnacondaModules = append( + anacondaPipeline.InstallerCustomizations.EnabledAnacondaModules, + anaconda.ModuleSecurity, + ) + } + + anacondaPipeline.Locale = img.Locale + + var rootfsImagePipeline *manifest.ISORootfsImg + switch img.InstallerCustomizations.ISORootfsType { + case manifest.SquashfsExt4Rootfs: + rootfsImagePipeline = manifest.NewISORootfsImg(buildPipeline, anacondaPipeline) + rootfsImagePipeline.Size = 4 * datasizes.GibiByte + default: + } + + bootTreePipeline := manifest.NewEFIBootTree(buildPipeline, img.InstallerCustomizations.Product, img.InstallerCustomizations.OSVersion) + bootTreePipeline.Platform = img.platform + bootTreePipeline.UEFIVendor = img.platform.GetUEFIVendor() + bootTreePipeline.ISOLabel = img.InstallerCustomizations.ISOLabel + + if img.Kickstart == nil { + img.Kickstart = &kickstart.Options{} + } + if img.Kickstart.Path == "" { + img.Kickstart.Path = osbuild.KickstartPathOSBuild + } + + bootTreePipeline.KernelOpts = []string{ + fmt.Sprintf("inst.stage2=hd:LABEL=%s", img.InstallerCustomizations.ISOLabel), + fmt.Sprintf("inst.ks=hd:LABEL=%s:%s", img.InstallerCustomizations.ISOLabel, img.Kickstart.Path), + // XXX: allow this to be configured + "inst.text", + // last console is what anaconda uses to disply its + // content, should we switch to tty0 by default here? + "console=tty0", "console=ttyS0", + } + if anacondaPipeline.InstallerCustomizations.FIPS { + bootTreePipeline.KernelOpts = append(bootTreePipeline.KernelOpts, "fips=1") + } + + isoTreePipeline := manifest.NewAnacondaInstallerISOTree(buildPipeline, anacondaPipeline, rootfsImagePipeline, bootTreePipeline) + isoTreePipeline.PartitionTable = efiBootPartitionTable(rng) + isoTreePipeline.Release = img.InstallerCustomizations.Release + isoTreePipeline.Kickstart = img.Kickstart + + isoTreePipeline.RootfsCompression = img.RootfsCompression + isoTreePipeline.RootfsType = img.InstallerCustomizations.ISORootfsType + + // For ostree installers, always put the kickstart file in the root of the ISO + isoTreePipeline.PayloadPath = "/container" + isoTreePipeline.PayloadRemoveSignatures = img.ContainerRemoveSignatures + + isoTreePipeline.ContainerSource = &img.InstallerPayload + isoTreePipeline.ISOBoot = img.InstallerCustomizations.ISOBoot + if anacondaPipeline.InstallerCustomizations.FIPS { + isoTreePipeline.KernelOpts = append(isoTreePipeline.KernelOpts, "fips=1") + } + + isoTreePipeline.InstallRootfsType = img.InstallRootfsType + + isoPipeline := manifest.NewISO(buildPipeline, isoTreePipeline, img.InstallerCustomizations.ISOLabel) + isoPipeline.SetFilename(img.filename) + isoPipeline.ISOBoot = img.InstallerCustomizations.ISOBoot + artifact := isoPipeline.Export() + + return artifact, nil +} diff --git a/pkg/manifest/anaconda_installer.go b/pkg/manifest/anaconda_installer.go index af53d254f7..44e560ccb1 100644 --- a/pkg/manifest/anaconda_installer.go +++ b/pkg/manifest/anaconda_installer.go @@ -7,6 +7,7 @@ import ( "github.com/osbuild/images/internal/common" "github.com/osbuild/images/pkg/arch" + "github.com/osbuild/images/pkg/container" "github.com/osbuild/images/pkg/customizations/fsnode" "github.com/osbuild/images/pkg/customizations/kickstart" "github.com/osbuild/images/pkg/customizations/users" @@ -55,6 +56,19 @@ type AnacondaInstaller struct { kernelName string kernelVer string + // some images (like bootc installers) know their path in + // advance + KernelPath string + InitramfsPath string + // bootc installer cannot use /root as installer home + InstallerHome string + + // BootcLivefsContainer is the container to use to + // as the base live filesystem. This is mutally exclusive + // with using RPMs. + BootcLivefsContainer *container.SourceSpec + bootcLivefsContainerSpecs []container.Spec + // Interactive defaults is a kickstart stage that can be provided, it // will be written to /usr/share/anaconda/interactive-defaults InteractiveDefaults *AnacondaInteractiveDefaults @@ -130,7 +144,20 @@ func (p *AnacondaInstaller) anacondaBootPackageSet() ([]string, error) { return packages, nil } +func (p *AnacondaInstaller) getContainerSources() []container.SourceSpec { + if p.BootcLivefsContainer == nil { + return nil + } + return []container.SourceSpec{*p.BootcLivefsContainer} +} + func (p *AnacondaInstaller) getBuildPackages(Distro) ([]string, error) { + // when using a bootc container for the livefs there is no + // need to get packages + if p.BootcLivefsContainer != nil { + return nil, nil + } + packages, err := p.anacondaBootPackageSet() if err != nil { return nil, fmt.Errorf("cannot get anaconda boot packages: %w", err) @@ -153,9 +180,19 @@ func (p *AnacondaInstaller) getBuildPackages(Distro) ([]string, error) { return packages, nil } +func (p *AnacondaInstaller) SetKernelVer(kVer string) { + p.kernelVer = kVer +} + // getPackageSetChain returns the packages to install // It will also include weak deps for the Live installer type func (p *AnacondaInstaller) getPackageSetChain(Distro) ([]rpmmd.PackageSet, error) { + // when using a bootc container for the livefs there is no + // need to get packages + if p.BootcLivefsContainer != nil { + return nil, nil + } + packages, err := p.anacondaBootPackageSet() if err != nil { return nil, fmt.Errorf("cannot get anaconda boot packages: %w", err) @@ -187,11 +224,12 @@ func (p *AnacondaInstaller) getPackageSpecs() rpmmd.PackageList { } func (p *AnacondaInstaller) serializeStart(inputs Inputs) error { - if len(p.packageSpecs) > 0 { + if len(p.packageSpecs) > 0 && len(p.bootcLivefsContainerSpecs) > 0 { return errors.New("AnacondaInstaller: double call to serializeStart()") } p.packageSpecs = inputs.Depsolved.Packages - if p.kernelName != "" { + // bootc-installers will get the kernelVer via introspection + if len(p.packageSpecs) > 0 && p.kernelName != "" { kernelPkg, err := p.packageSpecs.Package(p.kernelName) if err != nil { return fmt.Errorf("AnacondaInstaller: %w", err) @@ -199,15 +237,17 @@ func (p *AnacondaInstaller) serializeStart(inputs Inputs) error { p.kernelVer = kernelPkg.EVRA() } p.repos = append(p.repos, inputs.Depsolved.Repos...) + p.bootcLivefsContainerSpecs = inputs.Containers return nil } func (p *AnacondaInstaller) serializeEnd() { - if len(p.packageSpecs) == 0 { + if len(p.packageSpecs) == 0 && len(p.bootcLivefsContainerSpecs) == 0 { panic("serializeEnd() call when serialization not in progress") } p.kernelVer = "" p.packageSpecs = nil + p.bootcLivefsContainerSpecs = nil } func installerRootUser() osbuild.UsersStageOptionsUser { @@ -217,22 +257,35 @@ func installerRootUser() osbuild.UsersStageOptionsUser { } func (p *AnacondaInstaller) serialize() (osbuild.Pipeline, error) { - if len(p.packageSpecs) == 0 { + if len(p.packageSpecs) == 0 && len(p.bootcLivefsContainerSpecs) == 0 { return osbuild.Pipeline{}, fmt.Errorf("AnacondaInstaller: serialization not started") } + if len(p.packageSpecs) > 0 && len(p.bootcLivefsContainerSpecs) > 0 { + return osbuild.Pipeline{}, fmt.Errorf("AnacondaInstaller: using packages and containers at the same time is n") + } pipeline, err := p.Base.serialize() if err != nil { return osbuild.Pipeline{}, err } - options := osbuild.NewRPMStageOptions(p.repos) - // Documentation is only installed on live installer images - if p.Type != AnacondaInstallerTypeLive { - options.Exclude = &osbuild.Exclude{Docs: true} + if len(p.packageSpecs) > 0 { + options := osbuild.NewRPMStageOptions(p.repos) + // Documentation is only installed on live installer images + if p.Type != AnacondaInstallerTypeLive { + options.Exclude = &osbuild.Exclude{Docs: true} + } + + pipeline.AddStage(osbuild.NewRPMStage(options, osbuild.NewRpmStageSourceFilesInputs(p.packageSpecs))) + } else { + image := osbuild.NewContainersInputForSingleSource(p.bootcLivefsContainerSpecs[0]) + stage, err := osbuild.NewContainerDeployStage(image, &osbuild.ContainerDeployOptions{RemoveSignatures: true}) + if err != nil { + return pipeline, err + } + pipeline.AddStage(stage) } - pipeline.AddStage(osbuild.NewRPMStage(options, osbuild.NewRpmStageSourceFilesInputs(p.packageSpecs))) pipeline.AddStage(osbuild.NewBuildstampStage(&osbuild.BuildstampStageOptions{ Arch: p.platform.GetArch().String(), Product: p.InstallerCustomizations.Product, @@ -288,7 +341,11 @@ func (p *AnacondaInstaller) payloadStages() ([]*osbuild.Stage, error) { installUID := 0 installGID := 0 - installHome := "/root" + // bootc systems needs to be able to override this to /var/roothome + installHome := p.InstallerHome + if installHome == "" { + installHome = "/root" + } installShell := "/usr/libexec/anaconda/run-anaconda" installPassword := "" installUser := osbuild.UsersStageOptionsUser{ @@ -463,6 +520,12 @@ func (p *AnacondaInstaller) dracutStageOptions() (*osbuild.DracutStageOptions, e Extra: []string{"--xz"}, AddDrivers: p.InstallerCustomizations.AdditionalDrivers, } + if p.InitramfsPath != "" { + // dracut will by default write to /boot/initrmfs-$ver + // so we need to override if we have explicit paths + options.Extra = append(options.Extra, p.InitramfsPath) + } + options.AddModules = append(options.AddModules, p.InstallerCustomizations.AdditionalDracutModules...) if p.Biosdevname { diff --git a/pkg/manifest/anaconda_installer_iso_tree.go b/pkg/manifest/anaconda_installer_iso_tree.go index ce605da2a9..da8b7f9fe7 100644 --- a/pkg/manifest/anaconda_installer_iso_tree.go +++ b/pkg/manifest/anaconda_installer_iso_tree.go @@ -195,6 +195,10 @@ func (p *AnacondaInstallerISOTree) getInline() []string { return inlineData } func (p *AnacondaInstallerISOTree) getBuildPackages(_ Distro) ([]string, error) { + if p.anacondaPipeline.BootcLivefsContainer != nil { + return nil, nil + } + var packages []string switch p.RootfsType { case SquashfsExt4Rootfs, SquashfsRootfs: @@ -419,15 +423,23 @@ func (p *AnacondaInstallerISOTree) serialize() (osbuild.Pipeline, error) { })) } + // XXX: too indirect, ugly + if p.anacondaPipeline.KernelPath == "" { + p.anacondaPipeline.KernelPath = fmt.Sprintf("boot/vmlinuz-%s", p.anacondaPipeline.kernelVer) + } + if p.anacondaPipeline.InitramfsPath == "" { + p.anacondaPipeline.InitramfsPath = fmt.Sprintf("boot/initramfs-%s.img", p.anacondaPipeline.kernelVer) + } + inputName := "tree" copyStageOptions := &osbuild.CopyStageOptions{ Paths: []osbuild.CopyStagePath{ { - From: fmt.Sprintf("input://%s/boot/vmlinuz-%s", inputName, p.anacondaPipeline.kernelVer), + From: fmt.Sprintf("input://%s/%s", inputName, p.anacondaPipeline.KernelPath), To: "tree:///images/pxeboot/vmlinuz", }, { - From: fmt.Sprintf("input://%s/boot/initramfs-%s.img", inputName, p.anacondaPipeline.kernelVer), + From: fmt.Sprintf("input://%s/%s", inputName, p.anacondaPipeline.InitramfsPath), To: "tree:///images/pxeboot/initrd.img", }, }, diff --git a/test/bootc-fake-containers.yaml b/test/bootc-fake-containers.yaml index bc211668dc..c406864e87 100644 --- a/test/bootc-fake-containers.yaml +++ b/test/bootc-fake-containers.yaml @@ -1,12 +1,16 @@ --- containers: - &default_fake_container + image_types: ["qcow2", "ami", "raw", "gce", "vhd", "vmdk", "ova"] image_ref: "bootc-fake-imgref" arch: riscv64 info: &default_info os_release: + name: "bootc Test OS" id: "test-os" versionid: "1" + kernel_info: + version: 6.6 default_fs: "ext4" container_size: 10_000_000_000 - <<: *default_fake_container @@ -24,6 +28,7 @@ containers: <<: *default_info uefi_vendor: "uefi-vendor-aarch64" + # test build container - <<: *default_fake_container arch: x86_64 info: @@ -34,3 +39,13 @@ containers: build_container_info: os_release: id: "build-test-os" + + # test ISO installer + - <<: *default_fake_container + image_types: ["bootc-installer"] + arch: x86_64 + payload_container_ref: "payload-container-fake-ref" + - <<: *default_fake_container + image_types: ["bootc-installer"] + arch: aarch64 + payload_container_ref: "payload-container-fake-ref" diff --git a/test/data/manifest-checksums/bootc_test_os_1-aarch64-bootc_installer-empty b/test/data/manifest-checksums/bootc_test_os_1-aarch64-bootc_installer-empty new file mode 100644 index 0000000000..18eed3ea44 --- /dev/null +++ b/test/data/manifest-checksums/bootc_test_os_1-aarch64-bootc_installer-empty @@ -0,0 +1 @@ +db1d78a5248c7e7e84d1488d7315537886b95c6a diff --git a/test/data/manifest-checksums/bootc_test_os_1-x86_64-bootc_installer-empty b/test/data/manifest-checksums/bootc_test_os_1-x86_64-bootc_installer-empty new file mode 100644 index 0000000000..7914b8adb6 --- /dev/null +++ b/test/data/manifest-checksums/bootc_test_os_1-x86_64-bootc_installer-empty @@ -0,0 +1 @@ +6b8edec55f3154037ab6300c776105ffb83d0fdc From 9dff849048de3181263a2310213881fd7a93e87f Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Fri, 26 Sep 2025 10:19:46 +0200 Subject: [PATCH 3/5] image: tweak kernel isotree cmdline args This commit allows adding kernel commandline options to the AnacondaContainerInstaller and also tweak the existing options. Now that tests can append them we can drop e.g. `console=ttyS0`. --- pkg/image/anaconda_container_installer.go | 13 +++++++------ .../bootc_test_os_1-aarch64-bootc_installer-empty | 2 +- .../bootc_test_os_1-x86_64-bootc_installer-empty | 2 +- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/pkg/image/anaconda_container_installer.go b/pkg/image/anaconda_container_installer.go index 002b700124..97f4d51945 100644 --- a/pkg/image/anaconda_container_installer.go +++ b/pkg/image/anaconda_container_installer.go @@ -113,18 +113,19 @@ func (img *AnacondaContainerInstaller) InstantiateManifestFromContainer(m *manif img.Kickstart.Path = osbuild.KickstartPathOSBuild } - bootTreePipeline.KernelOpts = []string{ + kernelOpts := []string{ fmt.Sprintf("inst.stage2=hd:LABEL=%s", img.InstallerCustomizations.ISOLabel), fmt.Sprintf("inst.ks=hd:LABEL=%s:%s", img.InstallerCustomizations.ISOLabel, img.Kickstart.Path), - // XXX: allow this to be configured + "console=tty0", + // XXX: we want the graphical installer eventually, just + // need to figure out the dependencies "inst.text", - // last console is what anaconda uses to disply its - // content, should we switch to tty0 by default here? - "console=tty0", "console=ttyS0", } if anacondaPipeline.InstallerCustomizations.FIPS { - bootTreePipeline.KernelOpts = append(bootTreePipeline.KernelOpts, "fips=1") + kernelOpts = append(kernelOpts, "fips=1") } + kernelOpts = append(kernelOpts, img.InstallerCustomizations.KernelOptionsAppend...) + bootTreePipeline.KernelOpts = kernelOpts isoTreePipeline := manifest.NewAnacondaInstallerISOTree(buildPipeline, anacondaPipeline, rootfsImagePipeline, bootTreePipeline) isoTreePipeline.PartitionTable = efiBootPartitionTable(rng) diff --git a/test/data/manifest-checksums/bootc_test_os_1-aarch64-bootc_installer-empty b/test/data/manifest-checksums/bootc_test_os_1-aarch64-bootc_installer-empty index 18eed3ea44..7d6a8cefaf 100644 --- a/test/data/manifest-checksums/bootc_test_os_1-aarch64-bootc_installer-empty +++ b/test/data/manifest-checksums/bootc_test_os_1-aarch64-bootc_installer-empty @@ -1 +1 @@ -db1d78a5248c7e7e84d1488d7315537886b95c6a +0e5ba8dd30f80a431cf48405a7ff84eab6cc7a96 diff --git a/test/data/manifest-checksums/bootc_test_os_1-x86_64-bootc_installer-empty b/test/data/manifest-checksums/bootc_test_os_1-x86_64-bootc_installer-empty index 7914b8adb6..5d9468d072 100644 --- a/test/data/manifest-checksums/bootc_test_os_1-x86_64-bootc_installer-empty +++ b/test/data/manifest-checksums/bootc_test_os_1-x86_64-bootc_installer-empty @@ -1 +1 @@ -6b8edec55f3154037ab6300c776105ffb83d0fdc +57a85fa7b71308ed92a9d2ba6acac2fef1fa6559 From e75239b1b67e132f28da7b4f476aba38dc457fa5 Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Tue, 7 Oct 2025 12:33:56 +0200 Subject: [PATCH 4/5] distro: add `distro.ImageOptions.Bootc.InstallerPayloadRef` Moving the installer payload ref for bootc into the image options is a much nicer place than the previous way of having it as part `BootcDistro`. The alternative would be to make it part of `BootcAnacondaInstaller`. This would imply an explicit cast in all frontends so it seems this is preferable (and similar to how ostree commits are configured). --- cmd/gen-manifests/main.go | 10 +++++----- pkg/distro/bootc/bootc.go | 12 ++---------- pkg/distro/bootc/iso.go | 9 +++++++-- pkg/distro/distro.go | 5 +++++ 4 files changed, 19 insertions(+), 17 deletions(-) diff --git a/cmd/gen-manifests/main.go b/cmd/gen-manifests/main.go index f3e9444637..3631793840 100644 --- a/cmd/gen-manifests/main.go +++ b/cmd/gen-manifests/main.go @@ -650,11 +650,6 @@ func main() { panic(err) } } - if fakeBootcCnt.PayloadContainerRef != "" { - if err := distribution.SetInstallerPayload(fakeBootcCnt.PayloadContainerRef); err != nil { - panic(err) - } - } imgType, err := archi.GetImageType(imgTypeName) if err != nil { @@ -673,6 +668,11 @@ func main() { fmt.Printf("Skipping %s for %s/%s (reason: %v)\n", itConfig.Name, imgTypeName, distribution.Name(), reason) continue } + if fakeBootcCnt.PayloadContainerRef != "" { + itConfig.Options.Bootc = &distro.BootcImageOptions{ + InstallerPayloadRef: fakeBootcCnt.PayloadContainerRef, + } + } var repos []rpmmd.RepoConfig job := makeManifestJob(itConfig, imgType, distribution, repos, archName, cacheRoot, outputDir, contentResolve, metadata, tmpdirRoot) diff --git a/pkg/distro/bootc/bootc.go b/pkg/distro/bootc/bootc.go index 152b7cd915..c8c2d51e9e 100644 --- a/pkg/distro/bootc/bootc.go +++ b/pkg/distro/bootc/bootc.go @@ -31,10 +31,8 @@ import ( var _ = distro.Distro(&BootcDistro{}) type BootcDistro struct { - imgref string - buildImgref string - // XXX: wrong place? - payloadRef string + imgref string + buildImgref string sourceInfo *osinfo.Info buildSourceInfo *osinfo.Info @@ -85,12 +83,6 @@ func (d *BootcDistro) SetBuildContainer(imgref string) (err error) { return d.setBuildContainer(imgref, info) } -// XXX: wrong layer -func (d *BootcDistro) SetInstallerPayload(imgref string) error { - d.payloadRef = imgref - return nil -} - func (d *BootcDistro) setBuildContainer(imgref string, info *osinfo.Info) error { d.buildImgref = imgref d.buildSourceInfo = info diff --git a/pkg/distro/bootc/iso.go b/pkg/distro/bootc/iso.go index e73b12e63e..de2f5208aa 100644 --- a/pkg/distro/bootc/iso.go +++ b/pkg/distro/bootc/iso.go @@ -110,6 +110,11 @@ func (t *BootcAnacondaInstaller) Manifest(bp *blueprint.Blueprint, options distr if t.arch.distro.imgref == "" { return nil, nil, fmt.Errorf("internal error: no base image defined") } + if options.Bootc == nil || options.Bootc.InstallerPayloadRef == "" { + return nil, nil, fmt.Errorf("no installer payload bootc ref set") + } + payloadRef := options.Bootc.InstallerPayloadRef + containerSource := container.SourceSpec{ Source: t.arch.distro.imgref, Name: t.arch.distro.imgref, @@ -153,8 +158,8 @@ func (t *BootcAnacondaInstaller) Manifest(bp *blueprint.Blueprint, options distr img.InitramfsPath = fmt.Sprintf("lib/modules/%s/initramfs.img", t.arch.distro.sourceInfo.KernelInfo.Version) img.InstallerHome = "/var/roothome" payloadSource := container.SourceSpec{ - Source: t.arch.distro.payloadRef, - Name: t.arch.distro.payloadRef, + Source: payloadRef, + Name: payloadRef, Local: true, } img.InstallerPayload = payloadSource diff --git a/pkg/distro/distro.go b/pkg/distro/distro.go index e6e41049c7..1a2fa0d2e7 100644 --- a/pkg/distro/distro.go +++ b/pkg/distro/distro.go @@ -135,10 +135,15 @@ type ImageType interface { Manifest(bp *blueprint.Blueprint, options ImageOptions, repos []rpmmd.RepoConfig, seed *int64) (*manifest.Manifest, []string, error) } +type BootcImageOptions struct { + InstallerPayloadRef string `json:"installer_payload_ref,omitempty"` +} + // The ImageOptions specify options for a specific image build type ImageOptions struct { Size uint64 `json:"size"` OSTree *ostree.ImageOptions `json:"ostree,omitempty"` + Bootc *BootcImageOptions `json:"bootc,omitempty"` Subscription *subscription.ImageOptions `json:"subscription,omitempty"` Facts *facts.ImageOptions `json:"facts,omitempty"` PartitioningMode partition.PartitioningMode `json:"partitioning-mode,omitempty"` From a2ff171ca5d18823c32216cb8367ff3415d40cae Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Wed, 8 Oct 2025 10:47:44 +0200 Subject: [PATCH 5/5] bootc: add kernel customizations support to BootcAnacondaInstaller --- pkg/distro/bootc/iso.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/distro/bootc/iso.go b/pkg/distro/bootc/iso.go index de2f5208aa..dc0ce0a0a5 100644 --- a/pkg/distro/bootc/iso.go +++ b/pkg/distro/bootc/iso.go @@ -201,6 +201,9 @@ func (t *BootcAnacondaInstaller) Manifest(bp *blueprint.Blueprint, options distr anaconda.ModuleRuntime, anaconda.ModuleStorage, ) + if bpKernel := customizations.GetKernel(); bpKernel.Append != "" { + img.InstallerCustomizations.KernelOptionsAppend = append(img.InstallerCustomizations.KernelOptionsAppend, bpKernel.Append) + } img.Kickstart.OSTree = &kickstart.OSTree{ OSName: "default",