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"