#!/usr/bin/env bash set -euo pipefail URI="qemu:///system" IMGROOT="/var/lib/libvirt/images" BASEDIR="$IMGROOT/base" LABDIR="$IMGROOT/lab" TEMPLATE_DOMAIN="debian13-template" TEMPLATE_BASE="$BASEDIR/debian13-template.qcow2" usage() { cat <<'EOF' Usage: kvm-lab-create [--base template||||/abs/path.qcow2] [--start] Base resolution order: 1) template -> /var/lib/libvirt/images/base/debian13-template.qcow2 2) /abs/path.qcow2 -> that file 3) -> /var/lib/libvirt/images/base/ 4) -> vda disk path of that domain (virsh domblklist) 5) -> newest /var/lib/libvirt/images/base/-base-*.qcow2 EOF } die() { echo "ERROR: $*" >&2; exit 1; } NAME="${1:-}" shift || true BASESEL="template" START="no" while [[ $# -gt 0 ]]; do case "$1" in --base) BASESEL="${2:-}"; shift 2 ;; --start) START="yes"; shift ;; -h|--help) usage; exit 0 ;; *) die "Unknown argument: $1" ;; esac done [[ -n "$NAME" ]] || { usage; exit 1; } DISK="${LABDIR}/${NAME}.qcow2" XML_TMP="$(mktemp "/tmp/${NAME}.xml.XXXXXX")" defined="no" cleanup() { rm -f "$XML_TMP" if [[ "$defined" != "yes" && -e "$DISK" ]]; then echo "WARN: Removing orphan disk: $DISK" sudo rm -f "$DISK" || true fi } trap cleanup EXIT resolve_base() { local sel="$1" if [[ "$sel" == "template" ]]; then echo "$TEMPLATE_BASE"; return 0 fi if [[ "$sel" == /* ]]; then echo "$sel"; return 0 fi if [[ -f "$BASEDIR/$sel" ]]; then echo "$BASEDIR/$sel"; return 0 fi if virsh -c "$URI" dominfo "$sel" &>/dev/null; then local src src="$(virsh -c "$URI" domblklist "$sel" --details \ | awk '$2=="disk" && $3=="vda" {print $4; exit}')" [[ -n "$src" ]] || die "Domain '$sel' found but could not resolve vda disk path." echo "$src"; return 0 fi local latest latest="$(ls -1t "$BASEDIR"/"${sel}"-base-*.qcow2 2>/dev/null | head -n 1 || true)" [[ -n "$latest" ]] || die "Invalid --base '$sel' (no matching domain/file/promoted base in $BASEDIR)." echo "$latest" } # Preconditions [[ -d "$LABDIR" ]] || die "Lab directory missing: $LABDIR" [[ -d "$BASEDIR" ]] || die "Base directory missing: $BASEDIR" if ! virsh -c "$URI" dominfo "$TEMPLATE_DOMAIN" &>/dev/null; then die "Template domain not found in $URI: $TEMPLATE_DOMAIN" fi BASE="$(resolve_base "$BASESEL")" echo "Base selection: $BASESEL -> $BASE" [[ -f "$BASE" ]] || die "Base image not found: $BASE" sudo test -r "$BASE" || die "Base image not readable (via sudo): $BASE" [[ ! -e "$DISK" ]] || die "Disk already exists: $DISK" if virsh -c "$URI" dominfo "$NAME" &>/dev/null; then die "Domain already exists: $NAME" fi # Create overlay echo "Creating overlay disk: $DISK" sudo qemu-img create -f qcow2 -F qcow2 -b "$BASE" "$DISK" >/dev/null sudo chown libvirt-qemu:libvirt "$DISK" sudo chmod 0660 "$DISK" echo "Validating backing file..." sudo qemu-img info "$DISK" | grep -E "file format:|virtual size:|backing file:" || true # Clone template XML and adjust echo "Cloning domain XML from: $TEMPLATE_DOMAIN" virsh -c "$URI" dumpxml "$TEMPLATE_DOMAIN" > "$XML_TMP" # Update: name, disk source, network default # - replace ... once # - replace template disk path with overlay path (global) # - force network='default' sed -i \ -e "s|${TEMPLATE_DOMAIN}|${NAME}|" \ -e "s|${TEMPLATE_BASE}|${DISK}|g" \ -e "s|[^<]*<\/uuid>/d' "$XML_TMP" sed -i -E "s///g" "$XML_TMP" # Remove filesystem passthrough (if any) perl -0777 -i -pe 's/\n?//g' "$XML_TMP" # Validate substitutions happened grep -q "${NAME}" "$XML_TMP" || die "XML name substitution failed" grep -qF "$DISK" "$XML_TMP" || die "XML disk substitution failed" # Define domain echo "Defining domain: $NAME" virsh -c "$URI" define "$XML_TMP" >/dev/null defined="yes" # Optional start if [[ "$START" == "yes" ]]; then echo "Starting domain: $NAME" virsh -c "$URI" start "$NAME" >/dev/null fi echo "OK: Lab '$NAME' created (disk: $DISK)." if [[ "$START" != "yes" ]]; then echo "To start: virsh -c $URI start $NAME" fi