From dceb82f8f653d4109a31acc3feacc5da1eaf1890 Mon Sep 17 00:00:00 2001 From: Lukas Greve Date: Fri, 18 Sep 2026 20:04:49 +0200 Subject: [PATCH] Add build-image.sh: raw/qcow2 disk images without libvirt/virt-install MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .gitignore | 3 + README.md | 28 ++++++ build-image.sh | 184 ++++++++++++++++++++++++++++++++++++++++ deploy/deploy-distro.sh | 33 +------ deploy/select-dish.sh | 27 ++++++ 5 files changed, 246 insertions(+), 29 deletions(-) create mode 100755 build-image.sh create mode 100644 deploy/select-dish.sh diff --git a/.gitignore b/.gitignore index e465960..1076864 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,9 @@ cook/recipes/*.cfg cook/dishes/*.cfg +# Generated build products (created by build-image.sh) +build/ + # Python __pycache__/ *.py[cod] diff --git a/README.md b/README.md index c1bf4fa..63c13b4 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/build-image.sh b/build-image.sh new file mode 100755 index 0000000..a956669 --- /dev/null +++ b/build-image.sh @@ -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/.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 '_' 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 " diff --git a/deploy/deploy-distro.sh b/deploy/deploy-distro.sh index 861d2aa..d10ef94 100755 --- a/deploy/deploy-distro.sh +++ b/deploy/deploy-distro.sh @@ -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") diff --git a/deploy/select-dish.sh b/deploy/select-dish.sh new file mode 100644 index 0000000..0851cdf --- /dev/null +++ b/deploy/select-dish.sh @@ -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"