Add build-image.sh: raw/qcow2 disk images without libvirt/virt-install

Runs `livemedia-creator --no-virt --make-disk` directly on the host to
produce a disk image any VM manager can consume (e.g. via virtpull's
`virtpull local ... --wonder-vm NAME`), instead of deploy.sh's live
virt-install/libvirt session.

An earlier version wrapped this in `mock`, mirroring
.gitea/workflows/build-iso.yaml's ISO build. That doesn't work for
--make-disk: mock's chroot has no live systemd-udevd, so udev never
populates properties for the loop device livemedia-creator creates,
and blivet's device scan crashes. --make-iso never hits this since it
never touches block devices. Reproduced identically on Fedora 44 and
43 mock chroots, confirmed fixed by running directly on the host
instead (root required, for /dev/loop-control access).

Also fixes two real kickstart incompatibilities with the --no-virt
disk-image path (applied to a scratch copy, not the shared
ingredients): the `text` display-mode directive (needed for
virt-install's netinstall console) conflicts with livemedia-creator's
own display handling, and `part / --grow` with no explicit --size
crashes livemedia-creator's upfront disk-size calculation (works fine
under virt-install, which pre-creates the disk at a known size
instead) — now configurable via --root-size.

New --extra-ks FILE flag layers one-off local content (e.g. a bespoke
user/rootpw override) onto the scratch copy without touching tracked
ingredients, following the README's existing "bespoke dish not part
of the matrix" pattern.

Factored deploy/deploy-distro.sh's dish-picker into deploy/select-dish.sh,
shared by both scripts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Lukas Greve
2026-09-18 20:04:49 +02:00
co-authored by Claude Sonnet 5
parent a8228df780
commit dceb82f8f6
5 changed files with 246 additions and 29 deletions
+3
View File
@@ -2,6 +2,9 @@
cook/recipes/*.cfg
cook/dishes/*.cfg
# Generated build products (created by build-image.sh)
build/
# Python
__pycache__/
*.py[cod]
+28
View File
@@ -38,6 +38,34 @@ Creating domain...
`deploy.sh` regenerates the `recipes` and `dishes` (they are build products) before deploying.
After a successfull installation, the virtual machine will shutdown and be ready to use when powered on again.
## Building a disk image (no libvirt/virt-install)
`deploy.sh` drives a live `virt-install`/libvirt session. If you just want a
raw or qcow2 disk image — e.g. to hand to another VM manager such as
wonder-vm via virtpull's `virtpull local ... --wonder-vm NAME`
use `build-image.sh` instead. It runs `livemedia-creator --no-virt
--make-disk` directly on the host, producing a disk image instead of a
bootable ISO:
```
./build-image.sh [--as qcow2] [--root-size MiB] [--extra-ks FILE]
```
It picks a dish the same way `deploy.sh` does and writes the result under
`./build/`. Requires `lorax-lmc-novirt` (heavier than `deploy.sh`'s plain
QEMU/libvirt prerequisites, not installed by
`deploy/install-prerequisites-on-linux.sh`) and root — the `--no-virt` disk
install needs real loop-device access (`/dev/loop-control`), which a
`mock` chroot cannot provide (no live `systemd-udevd` inside it, which
breaks blivet's device scan) and neither can a rootless container.
`--root-size` (default 8192 MiB) sizes the root partition, needed because
livemedia-creator computes the image size up front, unlike virt-install
which pre-creates the disk at a known size. `--extra-ks FILE` appends
local content (e.g. a bespoke `user`/`rootpw` override) to the dish before
building, without touching the tracked ingredients — see the "bespoke
dish" pattern below; never commit a file used here if it carries real
credentials.
## Repository structure
This repository contains such files broken down as:
Executable
+184
View File
@@ -0,0 +1,184 @@
#!/bin/bash
# Build a raw (or qcow2) disk image from a dish, without libvirt/virt-install.
#
# Runs `livemedia-creator --no-virt --make-disk` directly on the host, so
# the result is a disk image you can hand to another VM manager directly
# (e.g. `virtpull local ./build/<dish>.img --wonder-vm NAME`) instead of a
# bootable ISO.
#
# Usage: ./build-image.sh [--as qcow2] [--root-size MiB] [--extra-ks FILE]
#
# Requires: lorax-lmc-novirt (livemedia-creator), qemu-img, and root (the
# --no-virt disk install needs real loop-device access via
# /dev/loop-control; individual commands below run under sudo — passwordless
# sudo, or run this whole script as root).
#
# Earlier iteration wrapped this in `mock` (mirroring
# .gitea/workflows/build-iso.yaml's ISO build) to pin the target release's
# toolchain. That does not work for --make-disk: mock's chroot has no live
# systemd-udevd, so udev never populates properties for the loop device
# livemedia-creator creates for the image, and blivet's device scan
# crashes (blivet/udev.py device_get_name: "TypeError: argument of type
# 'NoneType' is not a container or iterable") — reproduced identically on
# Fedora 44 and 43 mock chroots, and confirmed absent when run directly on
# the host instead. --make-iso never hits this: ISO building doesn't touch
# block devices at all, only --make-disk (and other disk/appliance modes)
# does. The consequence: this script now builds using whatever
# anaconda/lorax the host has installed — the kickstart's own repo/url
# lines still pin the actual package set to the target Fedora release
# regardless, same as any other direct (non-mock) livemedia-creator use.
set -euo pipefail
AS_FORMAT=""
ROOT_SIZE=8192
EXTRA_KS=""
while [[ $# -gt 0 ]]; do
case "$1" in
--as)
AS_FORMAT="$2"
shift 2
;;
--root-size)
ROOT_SIZE="$2"
shift 2
;;
--extra-ks)
EXTRA_KS="$2"
shift 2
;;
*)
echo "Unknown argument: $1"
echo "Usage: $0 [--as qcow2] [--root-size MiB] [--extra-ks FILE]"
exit 1
;;
esac
done
if [[ -n "$AS_FORMAT" && "$AS_FORMAT" != "qcow2" ]]; then
echo "--as only supports 'qcow2' (the build itself always produces raw)"
exit 1
fi
if ! [[ "$ROOT_SIZE" =~ ^[0-9]+$ ]]; then
echo "--root-size must be a whole number of MiB, got '$ROOT_SIZE'"
exit 1
fi
if [[ -n "$EXTRA_KS" && ! -f "$EXTRA_KS" ]]; then
echo "--extra-ks file not found: $EXTRA_KS"
exit 1
fi
for tool in livemedia-creator qemu-img; do
if ! command -v "$tool" &> /dev/null; then
echo "Missing required tool: $tool"
echo "Fedora: sudo dnf install lorax-lmc-novirt qemu-img"
exit 1
fi
done
if [[ $EUID -ne 0 ]] && ! sudo -n true 2>/dev/null; then
echo "This needs root (loop-device access for the --no-virt disk install)."
echo "Re-run as root, or ensure passwordless sudo is available."
exit 1
fi
# Regenerate recipes and dishes before building (they are build products)
if ! (cd cook && make all); then
echo "Failed to generate dishes in cook/ (see 'make all' in cook/)"
exit 1
fi
# Pick a dish (shared with deploy/deploy-distro.sh)
# shellcheck source=./deploy/select-dish.sh
source "$(dirname "$0")/deploy/select-dish.sh"
dish="$selected_dish"
# Derive the target release, same convention as
# deploy/deploy-distro.sh's find_fedora_iso() (used for --releasever, which
# substitutes @VERSION@ in bootloader/template text — the kickstart's own
# repo/url lines are what actually pin the package set).
if [[ "$dish" == *"rawhide"* ]]; then
releasever="rawhide"
elif [[ "$dish" =~ _([0-9]+) ]]; then
releasever="${BASH_REMATCH[1]}"
else
echo "Could not determine a Fedora release version from dish name '$dish'."
echo "Expected a '_<NN>' segment (e.g. desktop_44_standard...) or 'rawhide'."
exit 1
fi
out_dir="./build"
mkdir -p "$out_dir"
out_dir="$(cd "$out_dir" && pwd)"
# Adjustments needed only for this --no-virt disk-image path, applied to a
# scratch copy rather than the shared dish:
#
# 1. cook/ingredients/core/base.ks sets `text` for deploy/deploy-distro.sh's
# virt-install netinstall console; livemedia-creator manages the display
# mode itself and refuses a kickstart that also sets one ("The kickstart
# must not set a display mode").
#
# 2. cook/ingredients' storage layout uses `part / --grow` with no explicit
# --size: fine for virt-install, which pre-creates the disk at a known
# size for anaconda to grow into. livemedia-creator instead computes the
# total image size up front by summing partition sizes, and crashes
# (TypeError: unsupported operand type(s) for +: 'int' and 'NoneType')
# on a --grow partition with no size to add. Give it one via --root-size
# (default 8192 MiB; the desktop dishes likely need more).
#
# 3. --extra-ks appends arbitrary local content (e.g. a bespoke `user`/
# `rootpw`/`security` override) after the above. Kickstart's singular
# commands take the last occurrence, so this can override anything the
# dish already set without touching the shared ingredients — see the
# README's "bespoke dish not part of the matrix" pattern. Never commit
# a file used here if it carries real credentials.
disk_ks="$(mktemp "${TMPDIR:-/tmp}/${dish}.disk-build.XXXXXX.cfg")"
trap 'rm -f "$disk_ks"' EXIT
grep -v -E '^(text|cmdline|graphical)([[:space:]]|$)' "cook/dishes/${dish}.cfg" \
| awk -v size="$ROOT_SIZE" '
/^part / && /--grow/ && !/--size=/ { $0 = $0 " --size=" size }
{ print }
' > "$disk_ks"
if [[ -n "$EXTRA_KS" ]]; then
{
echo
cat "$EXTRA_KS"
} >> "$disk_ks"
fi
if command -v ksvalidator &> /dev/null; then
echo "Validating kickstart (informational; a build failure is the real test)..."
ksvalidator "$disk_ks" || true
fi
resultdir="$(mktemp -u "${TMPDIR:-/tmp}/lmc-result.XXXXXX")"
echo "Building disk image for '$dish' (target release $releasever)..."
sudo livemedia-creator --ks "$disk_ks" --no-virt --resultdir "$resultdir" \
--make-disk --image-name "${dish}.img" --releasever "$releasever"
# livemedia-creator's exact output filename depends on the installed lorax
# version; don't assume --image-name is honored verbatim, take whatever
# single disk image landed in the resultdir.
produced=$(sudo find "$resultdir" -maxdepth 1 -type f \( -iname '*.img' -o -iname '*disk*' \) | head -n 1)
if [[ -z "$produced" ]]; then
echo "livemedia-creator did not produce a recognizable disk image under $resultdir"
exit 1
fi
raw_out="${out_dir}/${dish}.img"
sudo cp --reflink=auto "$produced" "$raw_out"
sudo chown "$(id -u):$(id -g)" "$raw_out"
sudo rm -rf "$resultdir"
echo "Built: $raw_out"
if [[ "$AS_FORMAT" == "qcow2" ]]; then
qcow2_out="${out_dir}/${dish}.qcow2"
qemu-img convert -O qcow2 "$raw_out" "$qcow2_out"
echo "Converted: $qcow2_out"
final="$qcow2_out"
else
final="$raw_out"
fi
echo
echo "Next: virtpull local $final --wonder-vm <NAME>"
+4 -29
View File
@@ -141,35 +141,10 @@ fi
# Display the selected option (optional)
echo "You selected: $uri"
# Get a list of files in "dishes" directory
mapfile -t dish_name < <(find "cook/dishes/" -maxdepth 1 -type f \( -name "*guest-agents*" \) -printf "%f\n" | sed 's/\.[^.]*$//')
# Check if there are any files
if [ ${#dish_name[@]} -eq 0 ]; then
echo "No files found in the directory ../dishes."
exit 1
fi
# Display the files with numbered options
echo "Available files:"
for i in "${!dish_name[@]}"; do
echo "$((i + 1)). ${dish_name[$i]}"
done
# Prompt the user to select a file
read -r -p "Enter the number of the file you want to select: " choice
# Validate the user's choice
if ! [[ "$choice" =~ ^[0-9]+$ ]] || (( choice < 1 )) || (( choice > ${#dish_name[@]} )); then
echo "Invalid choice. Please enter a number from 1 to ${#dish_name[@]}."
exit 1
fi
# Get the selected filename
vm_name="${dish_name[$((choice - 1))]}"
# Output the selected filename
echo "You selected: $vm_name"
# Pick a dish (shared with build-image.sh)
# shellcheck source=./select-dish.sh
source "$(dirname "$0")/select-dish.sh"
vm_name="$selected_dish"
# Find Fedora ISO based on the dish name
fedora_iso=$(find_fedora_iso "$vm_name")
+27
View File
@@ -0,0 +1,27 @@
#!/bin/bash
# Lists cook/dishes/*guest-agents*.cfg and prompts the user to pick one.
# Meant to be sourced (not executed) from repo root, after `cook && make all`
# has regenerated dishes. Sets $selected_dish to the chosen dish's basename
# (no extension) on success, or exits non-zero.
mapfile -t dish_name < <(find "cook/dishes/" -maxdepth 1 -type f \( -name "*guest-agents*" \) -printf "%f\n" | sed 's/\.[^.]*$//')
if [ ${#dish_name[@]} -eq 0 ]; then
echo "No files found in cook/dishes/."
exit 1
fi
echo "Available dishes:"
for i in "${!dish_name[@]}"; do
echo "$((i + 1)). ${dish_name[$i]}"
done
read -r -p "Enter the number of the dish you want to select: " choice
if ! [[ "$choice" =~ ^[0-9]+$ ]] || (( choice < 1 )) || (( choice > ${#dish_name[@]} )); then
echo "Invalid choice. Please enter a number from 1 to ${#dish_name[@]}."
exit 1
fi
selected_dish="${dish_name[$((choice - 1))]}"
echo "You selected: $selected_dish"