diff --git a/data/distrodefs/bootc-generic/imagetypes.yaml b/data/distrodefs/bootc-generic/imagetypes.yaml index 20365d81fc..d7cd7893ed 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" @@ -180,16 +188,24 @@ 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"] 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 + # 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/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. diff --git a/pkg/distro/generic/bootc_imagetype.go b/pkg/distro/generic/bootc_imagetype.go index f621d5cc4c..c1a7234b93 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,101 @@ 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 + } + + // 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 } - img.RootfsCompression = "zstd" - img.RootfsType = manifest.SquashfsRootfs + 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 + } + 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 } - 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..f34dff0426 100644 --- a/pkg/distro/generic/bootc_test.go +++ b/pkg/distro/generic/bootc_test.go @@ -721,12 +721,12 @@ 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, }, - "os-tree": { + "image": { 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{} @@ -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 "generic-iso", "bootc-generic-iso": + t.Skipf("skipping %s: installer payload not supported ", imgTypeName) } imgOptions := distro.ImageOptions{ 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() 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) + } } } } 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 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{ diff --git a/pkg/manifest/iso_tree.go b/pkg/manifest/iso_tree.go index 149cc94cdd..c12b3ac00c 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/squashfs.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/squashfs.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..da6beb053f --- /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 (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/squashfs.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) +} diff --git a/test/bootc-fake-containers.yaml b/test/bootc-fake-containers.yaml index c157c04e18..3619bc2a02 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 @@ -89,15 +93,29 @@ 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 + 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 diff --git a/test/config-list.json b/test/config-list.json index bb0552bd39..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" ] } @@ -751,8 +751,7 @@ "bootc-*" ], "image-types": [ - "bootc-installer", - "bootc-generic-iso" + "bootc-installer" ] } }, @@ -771,7 +770,7 @@ "vmdk", "ova", "anaconda-iso", - "bootc-generic-iso", + "generic-iso", "pxe-tar-xz" ] } @@ -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 deleted file mode 100644 index 97480fa98e..0000000000 --- a/test/data/manifest-checksums/bootc_test_os_1-aarch64-bootc_generic_iso-bootc_empty +++ /dev/null @@ -1 +0,0 @@ -63d0d259ee0e5b361301b3a4f76abf5eb938fd5699f47b5e911a58afe99cedd7 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 deleted file mode 100644 index a7908f8d4b..0000000000 --- a/test/data/manifest-checksums/bootc_test_os_1-aarch64-bootc_generic_iso-bootc_remote_empty +++ /dev/null @@ -1 +0,0 @@ -2f6f38a83cb1581a900684d97ad37e6ccf674a364828e31efd0974744adc0243 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-aarch64-generic_iso-bootc_empty b/test/data/manifest-checksums/bootc_test_os_1-aarch64-generic_iso-bootc_empty new file mode 100644 index 0000000000..9a752217ea --- /dev/null +++ b/test/data/manifest-checksums/bootc_test_os_1-aarch64-generic_iso-bootc_empty @@ -0,0 +1 @@ +2f29b095806ba76886ac0efa8b433feb53efd17786584db40604092dfb7eb686 diff --git a/test/data/manifest-checksums/bootc_test_os_1-aarch64-generic_iso-bootc_remote_empty b/test/data/manifest-checksums/bootc_test_os_1-aarch64-generic_iso-bootc_remote_empty new file mode 100644 index 0000000000..32da8a7f3e --- /dev/null +++ b/test/data/manifest-checksums/bootc_test_os_1-aarch64-generic_iso-bootc_remote_empty @@ -0,0 +1 @@ +3769899b7518761b5cf32d730e9b63472c16303230a514577d177086b9591cb4 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-bootc_generic_iso-bootc_empty b/test/data/manifest-checksums/bootc_test_os_1-x86_64-bootc_generic_iso-bootc_empty deleted file mode 100644 index d153b65939..0000000000 --- a/test/data/manifest-checksums/bootc_test_os_1-x86_64-bootc_generic_iso-bootc_empty +++ /dev/null @@ -1 +0,0 @@ -6887fd245ac21a4d340c26092e91fb32968dc2aa010234d8f94e3792ec12fac8 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 deleted file mode 100644 index c63b902101..0000000000 --- a/test/data/manifest-checksums/bootc_test_os_1-x86_64-bootc_generic_iso-bootc_remote_empty +++ /dev/null @@ -1 +0,0 @@ -cac05dc1f31928bd0126801b6f3c3afb81f87658df25f35961a5d1780829b6a7 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 diff --git a/test/data/manifest-checksums/bootc_test_os_1-x86_64-generic_iso-bootc_empty b/test/data/manifest-checksums/bootc_test_os_1-x86_64-generic_iso-bootc_empty new file mode 100644 index 0000000000..37ea352513 --- /dev/null +++ b/test/data/manifest-checksums/bootc_test_os_1-x86_64-generic_iso-bootc_empty @@ -0,0 +1 @@ +d145f7012b97df0259734d9ef456c18a2b61c02ef6ef5118041b8312faa865a7 diff --git a/test/data/manifest-checksums/bootc_test_os_1-x86_64-generic_iso-bootc_remote_empty b/test/data/manifest-checksums/bootc_test_os_1-x86_64-generic_iso-bootc_remote_empty new file mode 100644 index 0000000000..76ca740bf1 --- /dev/null +++ b/test/data/manifest-checksums/bootc_test_os_1-x86_64-generic_iso-bootc_remote_empty @@ -0,0 +1 @@ +2196a8c520e9ea7ecb744201dc89521657d4ab47c628e44ba0327ca972808d11 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 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)