Skip to content
Merged
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
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ Flags:
--progress string type of progress bar to use (e.g. verbose,term) (default "auto")
--rootfs string Root filesystem type. If not given, the default configured in the source container image is used.
--target-arch string build for the given target architecture (experimental)
--type stringArray image types to build [ami, anaconda-iso, gce, iso, qcow2, raw, vhd, vmdk] (default [qcow2])
--type stringArray image types to build [ami, anaconda-iso, bootc-installer, gce, iso, qcow2, raw, vhd, vmdk] (default [qcow2])
--version version for bootc-image-builder

Global Flags:
Expand Down Expand Up @@ -172,7 +172,8 @@ The following image types are currently available via the `--type` argument:
| `ami` | [Amazon Machine Image](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/AMIs.html) |
| `qcow2` **(default)** | [QEMU](https://www.qemu.org/) |
| `vmdk` | [VMDK](https://en.wikipedia.org/wiki/VMDK) usable in vSphere, among others |
| `anaconda-iso` | An unattended Anaconda installer that installs to the first disk found. |
| `bootc-installer` | An installer ISO image based on the specified bootc container image. |
| `anaconda-iso` | An unattended Anaconda installer that installs to the first disk found build from RPMs. |
| `raw` | Unformatted [raw disk](https://en.wikipedia.org/wiki/Rawdisk). |
| `vhd` | [vhd](https://en.wikipedia.org/wiki/VHD_(file_format)) usable in Virtual PC, among others |
| `gce` | [GCE](https://cloud.google.com/compute/docs/images#custom_images) |
Expand Down
2 changes: 1 addition & 1 deletion bib/cmd/bootc-image-builder/legacy_iso.go
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ func manifestForISO(c *ManifestConfig, rng *rand.Rand) (*manifest.Manifest, erro

// The ref is not needed and will be removed from the ctor later
// in time
img := image.NewAnacondaContainerInstaller(platform, filename, containerSource, "")
img := image.NewAnacondaContainerInstallerLegacy(platform, filename, containerSource, "")
img.ContainerRemoveSignatures = true
img.RootfsCompression = "zstd"

Expand Down
24 changes: 17 additions & 7 deletions bib/cmd/bootc-image-builder/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"github.com/osbuild/images/pkg/bib/blueprintload"
"github.com/osbuild/images/pkg/cloud"
"github.com/osbuild/images/pkg/cloud/awscloud"
"github.com/osbuild/images/pkg/distro"
"github.com/osbuild/images/pkg/distro/bootc"
"github.com/osbuild/images/pkg/experimentalflags"
"github.com/osbuild/images/pkg/manifest"
Expand Down Expand Up @@ -93,6 +94,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")
installerPayloadRef, _ := cmd.Flags().GetString("installer-payload-ref")
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 @@ -153,21 +155,21 @@ func manifestFromCobra(cmd *cobra.Command, args []string, pbar progress.Progress
if imageTypes.Legacy() {
return manifestFromCobraForLegacyISO(imgref, buildImgref, imgType, rootFs, rpmCacheRoot, config, useLibrepo, cntArch)
}
return manifestFromCobraForDisk(imgref, buildImgref, imgType, rootFs, rpmCacheRoot, config, useLibrepo, cntArch)
return manifestFromCobraForDisk(imgref, buildImgref, installerPayloadRef, imgType, rootFs, rpmCacheRoot, config, useLibrepo, cntArch)
}

func manifestFromCobraForDisk(imgref, buildImgref, imgTypeStr, rootFs, rpmCacheRoot string, config *blueprint.Blueprint, useLibrepo bool, cntArch arch.Arch) ([]byte, *mTLSConfig, error) {
distro, err := bootc.NewBootcDistro(imgref)
func manifestFromCobraForDisk(imgref, buildImgref, installerPayloadRef, imgTypeStr, rootFs, rpmCacheRoot string, config *blueprint.Blueprint, useLibrepo bool, cntArch arch.Arch) ([]byte, *mTLSConfig, error) {
distri, err := bootc.NewBootcDistro(imgref)
if err != nil {
return nil, nil, err
}
if err := distro.SetBuildContainer(buildImgref); err != nil {
if err := distri.SetBuildContainer(buildImgref); err != nil {
return nil, nil, err
}
if err := distro.SetDefaultFs(rootFs); err != nil {
if err := distri.SetDefaultFs(rootFs); err != nil {
return nil, nil, err
}
archi, err := distro.GetArch(cntArch.String())
archi, err := distri.GetArch(cntArch.String())
if err != nil {
return nil, nil, err
}
Expand All @@ -193,7 +195,12 @@ func manifestFromCobraForDisk(imgref, buildImgref, imgTypeStr, rootFs, rpmCacheR
if err != nil {
return nil, nil, err
}
manifest, err := mg.Generate(config, imgType, nil)
imgOpts := &distro.ImageOptions{
Bootc: &distro.BootcImageOptions{
InstallerPayloadRef: installerPayloadRef,
},
}
manifest, err := mg.Generate(config, imgType, imgOpts)
if err != nil {
return nil, nil, err
}
Expand Down Expand Up @@ -506,6 +513,9 @@ 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: add --bootc-installer-payload-ref as alias to make it
// cmdline compatible with ibcli(?)
manifestCmd.Flags().String("installer-payload-ref", "", "bootc installer payload ref")
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
4 changes: 2 additions & 2 deletions bib/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ require (
github.com/hashicorp/go-version v1.7.0
github.com/osbuild/blueprint v1.16.0
github.com/osbuild/image-builder-cli v0.0.0-20250924085931-15de5139f521
github.com/osbuild/images v0.209.0
github.com/osbuild/images v0.211.0
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.10.1
github.com/spf13/pflag v1.0.10
github.com/stretchr/testify v1.11.1
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546
go.yaml.in/yaml/v3 v3.0.4
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546
)

require (
Expand Down
4 changes: 2 additions & 2 deletions bib/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -247,8 +247,8 @@ github.com/osbuild/blueprint v1.16.0 h1:f/kHih+xpeJ1v7wtIfzdHPZTsiXsqKeDQ1+rrue6
github.com/osbuild/blueprint v1.16.0/go.mod h1:HPlJzkEl7q5g8hzaGksUk7ifFAy9QFw9LmzhuFOAVm4=
github.com/osbuild/image-builder-cli v0.0.0-20250924085931-15de5139f521 h1:Mo1htXYyEoKrBQD+/RC/kluAWu4+E0oEjPorujVn/K8=
github.com/osbuild/image-builder-cli v0.0.0-20250924085931-15de5139f521/go.mod h1:oTn9T+bV9g/760hM/jX7AV0c4vuVIn6FjAnaVM9RzRo=
github.com/osbuild/images v0.209.0 h1:9BRf+N0op1WbQkc+7zVRBZxg4dqS4lty3i2stF3G9lo=
github.com/osbuild/images v0.209.0/go.mod h1:tZqcrs3eNUA0VPs1h3YCnbnpAskVVfo36CIi2USSfDs=
github.com/osbuild/images v0.211.0 h1:3BU7mMM7Iu81qZnq7y8luuIIOt707J9tF9DwCyOk9yM=
github.com/osbuild/images v0.211.0/go.mod h1:Cs7zFV8rmbVHn+19ArNdjd1AtFk+LC9dOOHuxiSLghw=
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
15 changes: 8 additions & 7 deletions bib/internal/imagetypes/imagetypes.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,14 @@ var supportedImageTypes = map[string]imageType{
// XXX: ideally we would look how to consolidate all
// knownledge about disk based image types into the images
// library
"ami": imageType{Export: "image"},
"qcow2": imageType{Export: "qcow2"},
"raw": imageType{Export: "image"},
"vmdk": imageType{Export: "vmdk"},
"vhd": imageType{Export: "vpc"},
"gce": imageType{Export: "gce"},
"ova": imageType{Export: "archive"},
"ami": imageType{Export: "image"},
"qcow2": imageType{Export: "qcow2"},
"raw": imageType{Export: "image"},
"vmdk": imageType{Export: "vmdk"},
"vhd": imageType{Export: "vpc"},
"gce": imageType{Export: "gce"},
"ova": imageType{Export: "archive"},
"bootc-installer": imageType{Export: "bootiso", ISO: true},
// the iso image types are RPM based and legacy/deprecated
"anaconda-iso": imageType{Export: "bootiso", ISO: true, Legacy: true},
"iso": imageType{Export: "bootiso", ISO: true, Legacy: true},
Expand Down
12 changes: 8 additions & 4 deletions bib/internal/imagetypes/imagetypes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,21 +57,25 @@ func TestImageTypes(t *testing.T) {
imageTypes: []string{"vmdk", "anaconda-iso"},
expectedErr: errors.New("cannot mix ISO/disk images in request [vmdk anaconda-iso]"),
},
"bad-mix-part-2": {
"bad-mix-2": {
imageTypes: []string{"vmdk", "bootc-installer"},
expectedErr: errors.New("cannot mix ISO/disk images in request [vmdk bootc-installer]"),
},
"bad-mix-3": {
imageTypes: []string{"ami", "iso"},
expectedErr: errors.New("cannot mix ISO/disk images in request [ami iso]"),
},
"bad-image-type": {
imageTypes: []string{"bad"},
expectedErr: errors.New(`unsupported image type "bad", valid types are ami, anaconda-iso, gce, iso, ova, qcow2, raw, vhd, vmdk`),
expectedErr: errors.New(`unsupported image type "bad", valid types are ami, anaconda-iso, bootc-installer, gce, iso, ova, qcow2, raw, vhd, vmdk`),
},
"bad-in-good": {
imageTypes: []string{"ami", "raw", "vmdk", "qcow2", "something-else-what-is-this"},
expectedErr: errors.New(`unsupported image type "something-else-what-is-this", valid types are ami, anaconda-iso, gce, iso, ova, qcow2, raw, vhd, vmdk`),
expectedErr: errors.New(`unsupported image type "something-else-what-is-this", valid types are ami, anaconda-iso, bootc-installer, gce, iso, ova, qcow2, raw, vhd, vmdk`),
},
"all-bad": {
imageTypes: []string{"bad1", "bad2", "bad3", "bad4", "bad5", "bad42"},
expectedErr: errors.New(`unsupported image type "bad1", valid types are ami, anaconda-iso, gce, iso, ova, qcow2, raw, vhd, vmdk`),
expectedErr: errors.New(`unsupported image type "bad1", valid types are ami, anaconda-iso, bootc-installer, gce, iso, ova, qcow2, raw, vhd, vmdk`),
},
}

Expand Down
122 changes: 122 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
import textwrap
from contextlib import ExitStack

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,120 @@ 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")
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that this build/boot test would ideally live in "images" but we don't have the infra there yet for this kind of local qemu tests.

@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",
])
# pylint: disable=too-many-locals
def test_bootc_installer_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: we need https://github.com/osbuild/images/pull/1786 or no kargs are added to anaconda
# XXX2: drop a bunch of the debug flags
#
# Use console=ttyS0 so that we see output in our debug
# logs. by default anaconda prints to the last console=
# from the kernel commandline
"append": "systemd.debug-shell=1 rd.systemd.debug-shell=1 inst.debug console=ttyS0",
},
},
}
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-core \
anaconda-dracut \
anaconda-install-img-deps \
biosdevname \
grub2-efi-x64-cdboot \
net-tools \
prefixdevname \
python3-mako \
lorax-templates-* \
squashfs-tools \
&& 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.
# If https://github.com/osbuild/osbuild/pull/2204 would get merged we
# can update images/ to set the correct efi_src_dirs and this can
# be removed (but its rather ugly).
# See also https://bugzilla.redhat.com/show_bug.cgi?id=1750708
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", "bootc-installer",
"--rootfs", "ext4",
"--installer-payload-ref", container_ref,
f"localhost/{container_tag}",
]
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
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
27 changes: 26 additions & 1 deletion test/test_manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def test_manifest_smoke(build_container, tc):


@pytest.mark.parametrize("tc", gen_testcases("anaconda-iso"))
def test_iso_manifest_smoke(build_container, tc):
def test_rpm_iso_manifest_smoke(build_container, tc):
testutil.pull_container(tc.container_ref, tc.target_arch)

output = subprocess.check_output([
Expand All @@ -71,6 +71,31 @@ def test_iso_manifest_smoke(build_container, tc):
assert [pipeline["name"] for pipeline in manifest["pipelines"]] == expected_pipeline_names


def test_bootc_iso_manifest_smoke(build_container):
container_ref = "quay.io/centos-bootc/centos-bootc:stream9"
# Note that this is not a realistic ref, a generic bootc
# image does not contain anaconda so this won't produce a
# working installer. For the purpose of the test to validate
# that we get a manifest with the right refs its good enough.
installer_payload_ref = "quay.io/centos-bootc/centos-bootc:stream10"
testutil.pull_container(container_ref)
testutil.pull_container(installer_payload_ref)

output = subprocess.check_output([
*testutil.podman_run_common,
build_container,
"manifest",
"--type=bootc-installer",
f"{container_ref}",
f"--installer-payload-ref={installer_payload_ref}",
])
manifest = json.loads(output)
# just some basic validation
expected_pipeline_names = ["build", "anaconda-tree", "efiboot-tree", "bootiso-tree", "bootiso"]
assert manifest["version"] == "2"
assert [pipeline["name"] for pipeline in manifest["pipelines"]] == expected_pipeline_names


@pytest.mark.parametrize("tc", gen_testcases("manifest"))
def test_manifest_disksize(tmp_path, build_container, tc):
testutil.pull_container(tc.container_ref, tc.target_arch)
Expand Down
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