Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 20 additions & 13 deletions bib/cmd/bootc-image-builder/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand All @@ -34,6 +32,8 @@ type ManifestConfig struct {
Imgref string
BuildImgref string

InstallerPayload string

ImageTypes imagetypes.ImageTypes

// Build config
Expand Down Expand Up @@ -83,24 +83,22 @@ 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,
UEFIVendor: c.SourceInfo.UEFIVendor,
}
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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
}

Expand Down
52 changes: 17 additions & 35 deletions bib/cmd/bootc-image-builder/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand Down
2 changes: 2 additions & 0 deletions bib/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 2 additions & 2 deletions bib/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand All @@ -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=
Expand Down
125 changes: 125 additions & 0 deletions test/test_build_iso.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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
1 change: 1 addition & 0 deletions test/vm.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading