diff --git a/bib/cmd/bootc-image-builder/image.go b/bib/cmd/bootc-image-builder/image.go index 78d3b9d7e..3e2f218ff 100644 --- a/bib/cmd/bootc-image-builder/image.go +++ b/bib/cmd/bootc-image-builder/image.go @@ -21,11 +21,9 @@ import ( "github.com/osbuild/images/pkg/manifest" "github.com/osbuild/images/pkg/osbuild" "github.com/osbuild/images/pkg/platform" - "github.com/osbuild/images/pkg/rpmmd" "github.com/osbuild/images/pkg/runner" "github.com/sirupsen/logrus" - "github.com/osbuild/bootc-image-builder/bib/internal/distrodef" "github.com/osbuild/bootc-image-builder/bib/internal/imagetypes" ) @@ -34,6 +32,8 @@ type ManifestConfig struct { Imgref string BuildImgref string + InstallerPayload string + ImageTypes imagetypes.ImageTypes // Build config @@ -83,17 +83,11 @@ func manifestForISO(c *ManifestConfig, rng *rand.Rand) (*manifest.Manifest, erro return nil, fmt.Errorf("pipeline: no base image defined") } - imageDef, err := distrodef.LoadImageDef(c.DistroDefPaths, c.SourceInfo.OSRelease.ID, c.SourceInfo.OSRelease.VersionID, "anaconda-iso") - if err != nil { - return nil, err - } - containerSource := container.SourceSpec{ Source: c.Imgref, Name: c.Imgref, Local: true, } - platform := &platform.Data{ Arch: c.Architecture, ImageFormat: platform.FORMAT_ISO, @@ -101,6 +95,10 @@ func manifestForISO(c *ManifestConfig, rng *rand.Rand) (*manifest.Manifest, erro } switch c.Architecture { case arch.ARCH_X86_64: + // XXX: for now + if c.SourceInfo.UEFIVendor == "" { + return nil, fmt.Errorf("UEFI vendor must be set for x86") + } platform.BIOSPlatform = "i386-pc" case arch.ARCH_AARCH64: // aarch64 always uses UEFI, so let's enforce the vendor @@ -123,6 +121,18 @@ func manifestForISO(c *ManifestConfig, rng *rand.Rand) (*manifest.Manifest, erro img := image.NewAnacondaContainerInstaller(platform, filename, containerSource, "") img.ContainerRemoveSignatures = true img.RootfsCompression = "zstd" + // kernelVer is used by dracut + img.KernelVer = c.SourceInfo.KernelInfo.Version + img.KernelPath = fmt.Sprintf("lib/modules/%s/vmlinuz", c.SourceInfo.KernelInfo.Version) + img.InitramfsPath = fmt.Sprintf("lib/modules/%s/initramfs.img", c.SourceInfo.KernelInfo.Version) + img.InstallerHome = "/var/roothome" + + payloadSource := container.SourceSpec{ + Source: c.InstallerPayload, + Name: c.InstallerPayload, + Local: true, + } + img.InstallerPayload = payloadSource if c.Architecture == arch.ARCH_X86_64 { img.InstallerCustomizations.ISOBoot = manifest.Grub2ISOBoot @@ -132,15 +142,12 @@ func manifestForISO(c *ManifestConfig, rng *rand.Rand) (*manifest.Manifest, erro img.InstallerCustomizations.OSVersion = c.SourceInfo.OSRelease.VersionID img.InstallerCustomizations.ISOLabel = labelForISO(&c.SourceInfo.OSRelease, &c.Architecture) - img.ExtraBasePackages = rpmmd.PackageSet{ - Include: imageDef.Packages, - } - var customizations *blueprint.Customizations if c.Config != nil { customizations = c.Config.Customizations } img.InstallerCustomizations.FIPS = customizations.GetFIPS() + var err error img.Kickstart, err = kickstart.New(customizations) if err != nil { return nil, err @@ -192,7 +199,7 @@ func manifestForISO(c *ManifestConfig, rng *rand.Rand) (*manifest.Manifest, erro } mf.Distro = foundDistro - _, err = img.InstantiateManifest(&mf, nil, foundRunner, rng) + _, err = img.InstantiateManifestFromContainers(&mf, []container.SourceSpec{containerSource}, foundRunner, rng) return &mf, err } diff --git a/bib/cmd/bootc-image-builder/main.go b/bib/cmd/bootc-image-builder/main.go index 2e2993005..c7d08842d 100644 --- a/bib/cmd/bootc-image-builder/main.go +++ b/bib/cmd/bootc-image-builder/main.go @@ -26,8 +26,8 @@ import ( "github.com/osbuild/images/pkg/cloud" "github.com/osbuild/images/pkg/cloud/awscloud" "github.com/osbuild/images/pkg/container" + "github.com/osbuild/images/pkg/depsolvednf" "github.com/osbuild/images/pkg/distro/bootc" - "github.com/osbuild/images/pkg/dnfjson" "github.com/osbuild/images/pkg/experimentalflags" "github.com/osbuild/images/pkg/manifest" "github.com/osbuild/images/pkg/manifestgen" @@ -43,15 +43,6 @@ import ( "github.com/osbuild/image-builder-cli/pkg/setup" ) -// all possible locations for the bib's distro definitions -// ./data/defs and ./bib/data/defs are for development -// /usr/share/bootc-image-builder/defs is for the production, containerized version -var distroDefPaths = []string{ - "./data/defs", - "./bib/data/defs", - "/usr/share/bootc-image-builder/defs", -} - var ( osGetuid = os.Getuid osGetgid = os.Getgid @@ -93,25 +84,13 @@ func inContainerOrUnknown() bool { return err == nil } -func makeManifest(c *ManifestConfig, solver *dnfjson.Solver, cacheRoot string) (manifest.OSBuildManifest, map[string][]rpmmd.RepoConfig, error) { +func makeManifest(c *ManifestConfig, solver *depsolvednf.Solver, cacheRoot string) (manifest.OSBuildManifest, map[string][]rpmmd.RepoConfig, error) { rng := createRand() mani, err := manifestForISO(c, rng) if err != nil { return nil, nil, fmt.Errorf("cannot get manifest: %w", err) } - // depsolve packages - depsolvedSets := make(map[string]dnfjson.DepsolveResult) - depsolvedRepos := make(map[string][]rpmmd.RepoConfig) - for name, pkgSet := range mani.GetPackageSetChains() { - res, err := solver.Depsolve(pkgSet, 0) - if err != nil { - return nil, nil, fmt.Errorf("cannot depsolve: %w", err) - } - depsolvedSets[name] = *res - depsolvedRepos[name] = res.Repos - } - // Resolve container - the normal case is that host and target // architecture are the same. However it is possible to build // cross-arch images by using qemu-user. This will run everything @@ -143,11 +122,11 @@ func makeManifest(c *ManifestConfig, solver *dnfjson.Solver, cacheRoot string) ( if c.UseLibrepo { opts.RpmDownloader = osbuild.RpmDownloaderLibrepo } - mf, err := mani.Serialize(depsolvedSets, containerSpecs, nil, &opts) + mf, err := mani.Serialize(nil, containerSpecs, nil, &opts) if err != nil { return nil, nil, fmt.Errorf("[ERROR] manifest serialization failed: %s", err.Error()) } - return mf, depsolvedRepos, nil + return mf, nil, nil } func saveManifest(ms manifest.OSBuildManifest, fpath string) (err error) { @@ -187,6 +166,7 @@ func manifestFromCobra(cmd *cobra.Command, args []string, pbar progress.Progress targetArch, _ := cmd.Flags().GetString("target-arch") rootFs, _ := cmd.Flags().GetString("rootfs") buildImgref, _ := cmd.Flags().GetString("build-container") + installerPayload, _ := cmd.Flags().GetString("installer-payload") useLibrepo, _ := cmd.Flags().GetBool("use-librepo") // If --local was given, warn in the case of --local or --local=true (true is the default), error in the case of --local=false @@ -356,16 +336,16 @@ func manifestFromCobra(cmd *cobra.Command, args []string, pbar progress.Progress } manifestConfig := &ManifestConfig{ - Architecture: cntArch, - Config: config, - ImageTypes: imageTypes, - Imgref: imgref, - BuildImgref: buildImgref, - DistroDefPaths: distroDefPaths, - SourceInfo: sourceinfo, - BuildSourceInfo: buildSourceinfo, - RootFSType: rootfsType, - UseLibrepo: useLibrepo, + Architecture: cntArch, + Config: config, + ImageTypes: imageTypes, + Imgref: imgref, + BuildImgref: buildImgref, + InstallerPayload: installerPayload, + SourceInfo: sourceinfo, + BuildSourceInfo: buildSourceinfo, + RootFSType: rootfsType, + UseLibrepo: useLibrepo, } manifest, repos, err := makeManifest(manifestConfig, solver, rpmCacheRoot) @@ -714,6 +694,8 @@ func buildCobraCmdline() (*cobra.Command, error) { manifestCmd.Flags().String("rpmmd", "/rpmmd", "rpm metadata cache directory") manifestCmd.Flags().String("target-arch", "", "build for the given target architecture (experimental)") manifestCmd.Flags().String("build-container", "", "Use a custom container for the image build") + // XXX: better name + manifestCmd.Flags().String("installer-payload", "", "Use this container for the installer payload") manifestCmd.Flags().StringArray("type", []string{"qcow2"}, fmt.Sprintf("image types to build [%s]", imagetypes.Available())) manifestCmd.Flags().Bool("local", true, "DEPRECATED: --local is now the default behavior, make sure to pull the container image before running bootc-image-builder") if err := manifestCmd.Flags().MarkHidden("local"); err != nil { diff --git a/bib/go.mod b/bib/go.mod index e17a46535..5881d23d5 100644 --- a/bib/go.mod +++ b/bib/go.mod @@ -135,3 +135,5 @@ require ( google.golang.org/protobuf v1.36.8 // indirect gopkg.in/ini.v1 v1.67.0 // indirect ) + +replace github.com/osbuild/images => github.com/mvo5/images v0.0.0-20250924104030-c68cb1db7550 diff --git a/bib/go.sum b/bib/go.sum index ac6a91671..7ba358501 100644 --- a/bib/go.sum +++ b/bib/go.sum @@ -233,6 +233,8 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mvo5/images v0.0.0-20250924104030-c68cb1db7550 h1:oum4THFaFAmjoEke3KzKopvtSTw0UIyTbi0BNOzFL64= +github.com/mvo5/images v0.0.0-20250924104030-c68cb1db7550/go.mod h1:KPiYBF0VrOXz5NAw6Lv4X170uN8wnOHpWuBzKT4jPrU= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= @@ -245,8 +247,6 @@ github.com/osbuild/blueprint v1.13.0 h1:blo22+S2ZX5bBmjGcRveoTUrV4Ms7kLfKyb32Wyu github.com/osbuild/blueprint v1.13.0/go.mod h1:HPlJzkEl7q5g8hzaGksUk7ifFAy9QFw9LmzhuFOAVm4= github.com/osbuild/image-builder-cli v0.0.0-20250331194259-63bb56e12db3 h1:M3yYunKH4quwJLQrnFo7dEwCTKorafNC+AUqAo7m5Yo= github.com/osbuild/image-builder-cli v0.0.0-20250331194259-63bb56e12db3/go.mod h1:0sEmiQiMo1ChSuOoeONN0RmsoZbQEvj2mlO2448gC5w= -github.com/osbuild/images v0.189.0 h1:fG9J9bxhdzkKkZ2EpW/LzT0YQBXY/kKiT99UpEzZhCo= -github.com/osbuild/images v0.189.0/go.mod h1:KPiYBF0VrOXz5NAw6Lv4X170uN8wnOHpWuBzKT4jPrU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= diff --git a/test/test_build_iso.py b/test/test_build_iso.py index d702e74db..34b0e0b64 100644 --- a/test/test_build_iso.py +++ b/test/test_build_iso.py @@ -1,12 +1,17 @@ import os +import random +import json import platform +import string import subprocess from contextlib import ExitStack +import textwrap import pytest # local test utils import testutil from containerbuild import build_container_fixture # pylint: disable=unused-import +from containerbuild import make_container from testcases import gen_testcases from vm import QEMU @@ -83,3 +88,123 @@ def test_iso_install_img_is_squashfs(tmp_path, image_type): # was an intermediate ext4 image "squashfs-root/LiveOS/rootfs.img" output = subprocess.check_output(["unsquashfs", "-ls", mount_point / "images/install.img"], text=True) assert "usr/bin/bootc" in output + + +@pytest.mark.skipif(platform.system() != "Linux", reason="boot test only runs on linux right now") +@pytest.mark.parametrize("container_ref", [ + "quay.io/centos-bootc/centos-bootc:stream10", + "quay.io/fedora/fedora-bootc:42", + "quay.io/centos-bootc/centos-bootc:stream9", +]) +def test_container_iso_installs(tmp_path, build_container, container_ref): + # XXX: duplicated from test_build_disk.py + username = "test" + password = "".join( + random.choices(string.ascii_uppercase + string.digits, k=18)) + ssh_keyfile_private_path = tmp_path / "ssh-keyfile" + ssh_keyfile_public_path = ssh_keyfile_private_path.with_suffix(".pub") + if not ssh_keyfile_private_path.exists(): + subprocess.run([ + "ssh-keygen", + "-N", "", + # be very conservative with keys for paramiko + "-b", "2048", + "-t", "rsa", + "-f", os.fspath(ssh_keyfile_private_path), + ], check=True) + ssh_pubkey = ssh_keyfile_public_path.read_text(encoding="utf8").strip() + + cfg = { + "customizations": { + "user": [ + { + "name": "root", + "key": ssh_pubkey, + # note that we have no "home" here for ISOs + }, { + "name": username, + "password": password, + "groups": ["wheel"], + }, + ], + "kernel": { + # XXX: console= needs to be default (why is it not?) + # XXX2: add inst.text automatically (or include all deps for a graphical install) + # XXX3: we need https://github.com/osbuild/images/pull/1786 or no kargs are added to anaconda + "append": f"systemd.debug-shell=1 rd.systemd.debug-shell=1 inst.debug", + }, + }, + } + config_json_path = tmp_path / "config.json" + config_json_path.write_text(json.dumps(cfg), encoding="utf-8") + + # create anaconda iso from base + cntf_path = tmp_path / "Containerfile" + cntf_path.write_text(textwrap.dedent(f"""\n + FROM {container_ref} + RUN dnf install -y \ + anaconda \ + anaconda-install-env-deps \ + anaconda-dracut \ + dracut-config-generic \ + dracut-network \ + net-tools \ + squashfs-tools \ + grub2-efi-x64-cdboot \ + python3-mako \ + lorax-templates-* \ + biosdevname \ + prefixdevname \ + && dnf clean all + # shim-x64 is marked installed but the files are not in the expected + # place for https://github.com/osbuild/osbuild/blob/v160/stages/org.osbuild.grub2.iso#L91, see + # workaround via reinstall, we could add a config to the grub2.iso + # stage to allow a different prefix that then would be used by + # anaconda. + # once https://github.com/osbuild/osbuild/pull/2202 is merged we + # can update images/ to set the correct efi_src_dir and this can + # be removed + RUN dnf reinstall -y shim-x64 + # lorax wants to create a symlink in /mnt which points to /var/mnt + # on bootc but /var/mnt does not exist on some images. + # + # If https://gitlab.com/fedora/bootc/base-images/-/merge_requests/294 + # gets merged this will be no longer needed + RUN mkdir /var/mnt + """), encoding="utf8") + + output_path = tmp_path / "output" + output_path.mkdir() + with make_container(tmp_path) as container_tag: + cmd = [ + *testutil.podman_run_common, + "-v", f"{config_json_path}:/config.json:ro", + "-v", f"{output_path}:/output", + "-v", "/var/tmp/osbuild-test-store:/store", # share the cache between builds + "-v", "/var/lib/containers/storage:/var/lib/containers/storage", + build_container, + "--type", "iso", + "--rootfs", "ext4", + "--installer-payload", container_ref, + f"localhost/{container_tag}", + ] + print(" ".join(cmd)) + subprocess.check_call(cmd) + + installer_iso_path = output_path / "bootiso" / "install.iso" + test_disk_path = installer_iso_path.with_name("test-disk.img") + with open(test_disk_path, "w", encoding="utf8") as fp: + fp.truncate(10_1000_1000_1000) + # install to test disk + with QEMU(test_disk_path, cdrom=installer_iso_path) as vm: + vm.start(wait_event="qmp:RESET", snapshot=False, use_ovmf=True) + vm.force_stop() + # boot test disk and do extremly simple check + with QEMU(test_disk_path) as vm: + vm.start(use_ovmf=True) + exit_status, _ = vm.run("true", user=username, password=password) + assert exit_status == 0 + #assert_kernel_args(vm, image_type) + exit_status, output = vm.run("bootc status", user="root", keyfile=ssh_keyfile_private_path) + assert exit_status == 0 + assert f"Booted image: {container_ref}" in output diff --git a/test/vm.py b/test/vm.py index a1be56a52..6157e3eb8 100644 --- a/test/vm.py +++ b/test/vm.py @@ -202,6 +202,7 @@ def wait_qmp_event(self, qmp_event): def force_stop(self): if self._qemu_p: self._qemu_p.kill() + self._qemu_p.wait() self._qemu_p = None self._address = None self._ssh_port = None