#!/usr/bin/env bash # ============================================================================ # qcrows-export — Export a QCrows image to qcow2, ISO, or PXE format # # Converts a .qcrows archive into: # - qcow2: QEMU disk image with rootfs installed, ready for VM boot # - iso: Bootable ISO for live USB media (hybrid MBR/EFI) # - pxe: PXE/TFTP directory for network boot via dnsmasq # # Usage: # qcrows-export --format qcow2 image.qcrows -o disk.qcow2 # qcrows-export --format iso image.qcrows -o live.iso # qcrows-export --format pxe image.qcrows -o /srv/tftp # # Prerequisites: # qcow2: qemu-img, parted, mkfs.ext4, mount, tar # iso: xorriso (or genisoimage), syslinux/mtools (for MBR boot), # grub-mkrescue (for EFI boot) # pxe: dnsmasq (for DHCP+TFTP), squashfs-tools (for rootfs packing) # ============================================================================ set -euo pipefail # ─── Version ──────────────────────────────────────────────────────────────── VERSION="0.2.0" # ─── Colors ───────────────────────────────────────────────────────────────── RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' CYAN='\033[0;36m' BLUE='\033[0;34m' NC='\033[0m' log() { echo -e "${GREEN}[qcrows-export]${NC} $*"; } warn() { echo -e "${YELLOW}[qcrows-export]${NC} WARNING: $*"; } error() { echo -e "${RED}[qcrows-export]${NC} ERROR: $*" >&2; } die() { error "$@"; exit 1; } # ─── Defaults ─────────────────────────────────────────────────────────────── FORMAT="" INPUT="" OUTPUT="" DISK_SIZE="" # calculated from rootfs size if empty BOOT_LABEL="QCROWS" ROOT_LABEL="ROOTFS" EFI_PARTITION=false COMPRESS_QCOW2=true VERBOSE=false PXE_SERVER_IP="" PXE_DEFAULT=true PXE_APPEND_CMDLINE="" # ─── Temp directory ──────────────────────────────────────────────────────── TMPDIR="" WORKDIR="" cleanup() { if [[ -n "$WORKDIR" && -d "$WORKDIR" ]]; then mountpoint -q "${WORKDIR}/mnt/rootfs" 2>/dev/null && umount -l "${WORKDIR}/mnt/rootfs" 2>/dev/null || true mountpoint -q "${WORKDIR}/mnt/efi" 2>/dev/null && umount -l "${WORKDIR}/mnt/efi" 2>/dev/null || true losetup -D 2>/dev/null || true fi [[ -n "$TMPDIR" && -d "$TMPDIR" ]] && rm -rf "$TMPDIR" } trap cleanup EXIT # ─── Helpers ──────────────────────────────────────────────────────────────── # Return the first existing file from candidate paths candidate_file() { local p for p in "$@"; do [[ -f "$p" ]] && { echo "$p"; return 0; } done return 1 } # Return the first existing file from candidate paths (supports glob expansion) first_existing_file() { local candidate match for candidate in "$@"; do for match in $candidate; do if [[ -f "$match" ]]; then echo "$match" return 0 fi done done return 1 } # Copy source to destination only if source exists copy_if_exists() { local src="$1" dest="$2" [[ -f "$src" ]] && cp "$src" "$dest" } # Construct kernel command line from base params + optional boot-params.conf build_cmdline() { local base="$1" local params_file="$2" if [[ -n "$params_file" && -f "$params_file" ]]; then local extra extra=$(tr '\n' ' ' < "$params_file" | sed 's/ */ /g; s/^ *//; s/ *$//') echo "${base} ${extra}" else echo "$base" fi } # ─── Usage ────────────────────────────────────────────────────────────────── usage() { cat </dev/null || missing+=(qemu-img) command -v parted &>/dev/null || missing+=(parted) command -v mkfs.ext4 &>/dev/null || missing+=(mkfs.ext4) command -v mount &>/dev/null || missing+=(mount) command -v tar &>/dev/null || missing+=(tar) [[ ${#missing[@]} -gt 0 ]] && die "Missing prerequisites for qcow2 export: ${missing[*]}. Install qemu-utils, parted, e2fsprogs." } check_prereqs_iso() { local -a missing=() # ISO tool detection — ordered preference local -a iso_tools=(xorriso genisoimage mkisofs) ISO_TOOL="" for tool in "${iso_tools[@]}"; do if command -v "$tool" &>/dev/null; then ISO_TOOL="$tool" break fi done [[ -z "$ISO_TOOL" ]] && missing+=("xorriso or genisoimage") # MBR boot requires isolinux if [[ "$EFI_PARTITION" == "false" ]]; then candidate_file /usr/lib/ISOLINUX/isolinux.bin /usr/share/syslinux/isolinux.bin &>/dev/null \ || missing+=(isolinux/syslinux) fi # EFI boot requires grub-mkrescue [[ "$EFI_PARTITION" == "true" ]] && { command -v grub-mkrescue &>/dev/null || missing+=(grub-mkrescue); } [[ ${#missing[@]} -gt 0 ]] && die "Missing prerequisites for ISO export: ${missing[*]}. Install xorriso, syslinux, and/or grub2." } # ─── Extract QCrows Archive ──────────────────────────────────────────────── extract_qcrows() { local archive="$1" local dest="$2" log "Extracting QCrows archive: $(basename "$archive")" case "$archive" in *.gz) tar xzf "$archive" -C "$dest" 2>/dev/null || tar xf "$archive" -C "$dest" 2>/dev/null ;; *) tar xf "$archive" -C "$dest" 2>/dev/null ;; esac # Descend into subdirectory if the archive wraps everything in one local -a top_entries mapfile -t top_entries < <(find "$dest" -maxdepth 1 -type d ! -path "$dest") if [[ ${#top_entries[@]} -eq 1 && -f "${top_entries[0]}/metadata.toml" ]]; then mv "${top_entries[0]}"/* "$dest/" 2>/dev/null || true rmdir "${top_entries[0]}" 2>/dev/null || true fi # Validate required components [[ -f "${dest}/metadata.toml" ]] || die "metadata.toml not found in archive" [[ -f "${dest}/hashes.sha256" ]] || die "hashes.sha256 not found in archive" # Find rootfs ROOTFS_FILE="" local -a rootfs_candidates=() mapfile -t rootfs_candidates < <(find "$dest" -maxdepth 1 -name 'rootfs*' -type f 2>/dev/null) [[ ${#rootfs_candidates[@]} -gt 0 ]] || die "rootfs not found in archive" ROOTFS_FILE="${rootfs_candidates[0]}" log "Found rootfs: $(basename "$ROOTFS_FILE")" # Find kernel KERNEL_FILE="$(first_existing_file "${dest}/kernel/vmlinuz" "${dest}/kernel/vmlinux" || true)" [[ -n "$KERNEL_FILE" ]] || die "kernel not found in archive (required for bootable export)" log "Found kernel: $(basename "$KERNEL_FILE")" # Find initrd INITRD_FILE="" local -a initrd_candidates=() mapfile -t initrd_candidates < <(find "$dest" -maxdepth 1 \( -name 'initrd*' -o -name 'initramfs*' \) -type f 2>/dev/null) if [[ ${#initrd_candidates[@]} -gt 0 ]]; then INITRD_FILE="${initrd_candidates[0]}" log "Found initrd: $(basename "$INITRD_FILE")" else warn "No initrd found — ISO boot may require initramfs" fi # Find boot-params BOOT_PARAMS_FILE="" [[ -f "${dest}/boot-params.conf" ]] && { BOOT_PARAMS_FILE="${dest}/boot-params.conf"; log "Found boot-params.conf"; } # Read kernel version from metadata KERNEL_VERSION=$(awk '/\[ kernel \]/{found=1} found && /^version/{print; exit}' "$dest/metadata.toml" | sed 's/.*= *//' | tr -d '"' || echo "unknown") if [[ -z "$KERNEL_VERSION" || "$KERNEL_VERSION" == "unknown" ]]; then KERNEL_VERSION=$(strings "$KERNEL_FILE" 2>/dev/null | grep -oP '^\d+\.\d+\.\d+' | head -1 || echo "unknown") fi log "Kernel version: ${KERNEL_VERSION}" # Verify hashes log "Verifying archive integrity..." (cd "$dest" && sha256sum --quiet -c hashes.sha256 2>/dev/null) || warn "SHA-256 verification reported mismatches — proceed with caution" } # ─── Compute disk size from rootfs ───────────────────────────────────────── compute_disk_size() { if [[ -n "$DISK_SIZE" ]]; then echo "$DISK_SIZE" return fi local rootfs_bytes rootfs_bytes=$(stat --format='%s' "$ROOTFS_FILE" 2>/dev/null || stat -f '%z' "$ROOTFS_FILE" 2>/dev/null || echo 0) # Assume 2.5x compression ratio for uncompressed rootfs size estimate local estimated_mb=$(( (rootfs_bytes * 5 / 2) / (1024 * 1024) )) # Add 256MB headroom, minimum 512MB local total_mb=$(( estimated_mb + 256 )) [[ $total_mb -lt 512 ]] && total_mb=512 # Round up to next 128MB boundary total_mb=$(( ((total_mb / 128) + 1) * 128 )) echo "${total_mb}M" } # ─── Export to qcow2 ─────────────────────────────────────────────────────── export_qcow2() { local archive="$1" local output="$2" check_prereqs_qcow2 WORKDIR="$(mktemp -d /tmp/qcrows-export-qcow2-XXXXXX)" local extract_dir="${WORKDIR}/extracted" mkdir -p "$extract_dir" extract_qcrows "$archive" "$extract_dir" local disk_size disk_size=$(compute_disk_size) log "Disk size: ${disk_size}" # ── Step 1: Create raw disk image ───────────────────────────────────── local raw_img="${WORKDIR}/disk.raw" log "Creating raw disk image (${disk_size})..." qemu-img create -f raw "$raw_img" "$disk_size" # ── Step 2: Partition ───────────────────────────────────────────────── log "Partitioning disk..." local efi_part="" root_part="" if [[ "$EFI_PARTITION" == "true" ]]; then parted -s "$raw_img" mklabel gpt parted -s "$raw_img" mkpart ESP fat32 1MiB 513MiB parted -s "$raw_img" set 1 esp on parted -s "$raw_img" mkpart root ext4 513MiB 100% else parted -s "$raw_img" mklabel msdos parted -s "$raw_img" mkpart primary ext4 1MiB 100% parted -s "$raw_img" set 1 boot on fi # ── Step 3: Set up loop device ──────────────────────────────────────── log "Setting up loop device..." local loop_dev loop_dev=$(losetup --find --show --partscan "$raw_img") sleep 1 partprobe "$loop_dev" 2>/dev/null || true sleep 1 local -a partitions=() mapfile -t partitions < <(ls "${loop_dev}p"* 2>/dev/null || echo "") if [[ "$EFI_PARTITION" == "true" ]]; then efi_part="${partitions[0]}" root_part="${partitions[1]}" log "Formatting EFI partition..." mkfs.vfat -F 32 -n "$BOOT_LABEL" "$efi_part" 2>/dev/null || die "Failed to format EFI partition" else root_part="${partitions[0]}" fi log "Formatting root partition..." mkfs.ext4 -L "$ROOT_LABEL" -q "$root_part" || die "Failed to format root partition" # ── Step 4: Mount and install rootfs ────────────────────────────────── local mnt="${WORKDIR}/mnt" mkdir -p "${mnt}/rootfs" log "Mounting root partition..." mount "$root_part" "${mnt}/rootfs" log "Extracting rootfs onto disk..." case "$ROOTFS_FILE" in *.tar.gz|*.tgz) tar xzf "$ROOTFS_FILE" -C "${mnt}/rootfs" ;; *.tar.xz|*.txz) tar xJf "$ROOTFS_FILE" -C "${mnt}/rootfs" ;; *.tar.zst) tar --zstd -xf "$ROOTFS_FILE" -C "${mnt}/rootfs" ;; *) tar xf "$ROOTFS_FILE" -C "${mnt}/rootfs" ;; esac # ── Step 5: Install kernel ─────────────────────────────────────────── log "Installing kernel into /boot..." mkdir -p "${mnt}/rootfs/boot" cp "$KERNEL_FILE" "${mnt}/rootfs/boot/vmlinuz-${KERNEL_VERSION}" copy_if_exists "${extract_dir}/kernel/config" "${mnt}/rootfs/boot/config-${KERNEL_VERSION}" [[ -n "$INITRD_FILE" ]] && cp "$INITRD_FILE" "${mnt}/rootfs/boot/initrd-${KERNEL_VERSION}" # ── Step 6: Create fstab ───────────────────────────────────────────── log "Generating /etc/fstab..." local -a fstab_lines=("LABEL=${ROOT_LABEL} / ext4 defaults,noatime 0 1") [[ "$EFI_PARTITION" == "true" ]] && fstab_lines+=("LABEL=${BOOT_LABEL} /boot/efi vfat defaults,noatime 0 2") printf '%s\n' "# /etc/fstab — QCrows export disk layout" "${fstab_lines[@]}" > "${mnt}/rootfs/etc/fstab" # ── Step 7: Install bootloader ─────────────────────────────────────── local cmdline cmdline=$(build_cmdline "root=LABEL=${ROOT_LABEL} rw console=ttyS0 console=tty0" "$BOOT_PARAMS_FILE") if [[ "$EFI_PARTITION" == "false" ]]; then # MBR boot with extlinux log "Installing extlinux bootloader..." mkdir -p "${mnt}/rootfs/boot/extlinux" local -a extlinux_lines=( "# extlinux.conf — QCrows boot configuration" "DEFAULT kata" "TIMEOUT 30" "" "LABEL kata" " KERNEL /boot/vmlinuz-${KERNEL_VERSION}" ) [[ -n "$INITRD_FILE" ]] && extlinux_lines+=(" INITRD /boot/initrd-${KERNEL_VERSION}") extlinux_lines+=(" APPEND ${cmdline}") printf '%s\n' "${extlinux_lines[@]}" > "${mnt}/rootfs/boot/extlinux/extlinux.conf" # Install extlinux if available command -v extlinux &>/dev/null && extlinux --install "${mnt}/rootfs/boot/extlinux" 2>/dev/null \ || warn "extlinux install failed — install syslinux-extlinux" # Install MBR boot code local mbr_bin mbr_bin=$(candidate_file /usr/lib/syslinux/mbr/mbr.bin /usr/share/syslinux/mbr.bin || true) [[ -n "$mbr_bin" ]] && dd if="$mbr_bin" of="$raw_img" bs=440 count=1 conv=notrunc 2>/dev/null \ || warn "MBR boot code install failed — install syslinux" else # EFI boot with GRUB log "Setting up EFI bootloader..." mkdir -p "${mnt}/efi" mount "$efi_part" "${mnt}/efi" mkdir -p "${mnt}/efi/EFI/BOOT" cp "$KERNEL_FILE" "${mnt}/efi/EFI/BOOT/vmlinuz" [[ -n "$INITRD_FILE" ]] && cp "$INITRD_FILE" "${mnt}/efi/EFI/BOOT/initrd" # Build GRUB config local -a grub_lines=( "# grub.cfg — QCrows EFI boot configuration" "set timeout=3" "set default=0" "" "menuentry \"Kata Container Image (${KERNEL_VERSION})\" {" " linux /EFI/BOOT/vmlinuz ${cmdline}" ) [[ -n "$INITRD_FILE" ]] && grub_lines+=(" initrd /EFI/BOOT/initrd") grub_lines+=("}") printf '%s\n' "${grub_lines[@]}" > "${mnt}/efi/EFI/BOOT/grub.cfg" # Locate and install GRUB EFI binary local grub_x64 grub_x64=$(candidate_file \ "/usr/lib/grub/x86_64-efi/grubx64.efi" \ "/usr/share/grub/x86_64-efi/grubx64.efi" \ "/boot/efi/EFI/fedora/grubx64.efi" \ "/boot/efi/EFI/debian/grubx64.efi" \ "/boot/efi/EFI/ubuntu/grubx64.efi" \ || true) if [[ -n "$grub_x64" ]]; then cp "$grub_x64" "${mnt}/efi/EFI/BOOT/grubx64.efi" log "Installed GRUB EFI bootloader" else warn "No GRUB EFI binary found — EFI boot requires a bootloader" warn "Install grub2-efi-x64 or similar package" fi umount "${mnt}/efi" fi # ── Step 8: Unmount ────────────────────────────────────────────────── log "Syncing and unmounting..." sync umount "${mnt}/rootfs" # Detach loop device losetup -d "$loop_dev" 2>/dev/null || true # ── Step 9: Convert to qcow2 ──────────────────────────────────────── log "Converting to qcow2..." local -a qcow2_opts=() [[ "$COMPRESS_QCOW2" == "true" ]] && qcow2_opts+=(-c) qemu-img convert -f raw -O qcow2 "${qcow2_opts[@]}" "$raw_img" "$output" local final_size final_size=$(du -sh "$output" | cut -f1) # ── Summary ────────────────────────────────────────────────────────── echo "" echo -e "${GREEN}╔══════════════════════════════════════════════╗${NC}" echo -e "${GREEN}║ QCrows → qcow2 export complete ║${NC}" echo -e "${GREEN}╚══════════════════════════════════════════════╝${NC}" echo "" echo -e " Output: ${CYAN}${output}${NC}" echo -e " Size: ${CYAN}${final_size}${NC}" echo -e " Disk size: ${CYAN}${disk_size}${NC}" echo -e " Kernel: ${CYAN}${KERNEL_VERSION}${NC}" echo -e " Boot: ${CYAN}$([ "$EFI_PARTITION" == "true" ] && echo "EFI/GPT" || echo "MBR/extlinux")${NC}" echo "" echo -e " Boot with QEMU:" echo -e " ${CYAN}qemu-system-x86_64 -m 1G -smp 2 -drive file=${output},format=qcow2 -enable-kvm${NC}" echo "" } # ─── Export to ISO ────────────────────────────────────────────────────────── export_iso() { local archive="$1" local output="$2" check_prereqs_iso WORKDIR="$(mktemp -d /tmp/qcrows-export-iso-XXXXXX)" local extract_dir="${WORKDIR}/extracted" local iso_root="${WORKDIR}/iso" mkdir -p "$extract_dir" "$iso_root" extract_qcrows "$archive" "$extract_dir" # ── Step 1: Build ISO directory structure ───────────────────────────── log "Building ISO directory structure..." mkdir -p "${iso_root}/boot/isolinux" mkdir -p "${iso_root}/boot/grub" mkdir -p "${iso_root}/live" mkdir -p "${iso_root}/kata" # ── Step 2: Copy kernel and initrd ─────────────────────────────────── log "Installing kernel for ISO boot..." cp "$KERNEL_FILE" "${iso_root}/boot/vmlinuz" cp "$KERNEL_FILE" "${iso_root}/live/vmlinuz" if [[ -n "$INITRD_FILE" ]]; then cp "$INITRD_FILE" "${iso_root}/boot/initrd" cp "$INITRD_FILE" "${iso_root}/live/initrd" fi copy_if_exists "${extract_dir}/kernel/config" "${iso_root}/boot/" # ── Step 3: Copy rootfs and Kata deployment bundle ─────────────────── log "Bundling rootfs for live boot..." cp "$ROOTFS_FILE" "${iso_root}/live/rootfs.tar.gz" # Place components in /kata for direct Kata deployment cp "$ROOTFS_FILE" "${iso_root}/kata/" [[ -n "$INITRD_FILE" ]] && cp "$INITRD_FILE" "${iso_root}/kata/" cp "$KERNEL_FILE" "${iso_root}/kata/" copy_if_exists "${extract_dir}/kernel/config" "${iso_root}/kata/" # Copy QCrows metadata for in-ISO reference cp "${extract_dir}/metadata.toml" "${iso_root}/kata/" copy_if_exists "${extract_dir}/menu.toml" "${iso_root}/kata/" copy_if_exists "${extract_dir}/build.toml" "${iso_root}/kata/" copy_if_exists "${extract_dir}/boot-params.conf" "${iso_root}/kata/" # ── Step 4: Build kernel command line ──────────────────────────────── local cmdline cmdline=$(build_cmdline "boot=live components quiet" "$BOOT_PARAMS_FILE") # ── Step 5: Create isolinux config (MBR boot) ──────────────────────── log "Creating isolinux configuration..." local -a isolinux_lines=( "# isolinux.cfg — QCrows live boot menu" "DEFAULT kata" "TIMEOUT 30" "PROMPT 0" "" "LABEL kata" " KERNEL /boot/vmlinuz" ) [[ -n "$INITRD_FILE" ]] && isolinux_lines+=(" INITRD /boot/initrd") isolinux_lines+=(" APPEND ${cmdline}") printf '%s\n' "${isolinux_lines[@]}" > "${iso_root}/boot/isolinux/isolinux.cfg" # ── Step 6: Create GRUB config (EFI boot) ─────────────────────────── log "Creating GRUB configuration..." local -a grub_lines=( "# grub.cfg — QCrows EFI boot menu" "set timeout=3" "set default=0" "set graphics=off" "" "menuentry \"Kata Container Image (${KERNEL_VERSION})\" {" " linux /boot/vmlinuz ${cmdline}" ) [[ -n "$INITRD_FILE" ]] && grub_lines+=(" initrd /boot/initrd") grub_lines+=("}") # Secondary entry for Kata deployment mode grub_lines+=( "" "menuentry \"Kata Deployment Mode (load rootfs from /kata/)\" {" " linux /boot/vmlinuz kata-deploy=live quiet" ) [[ -n "$INITRD_FILE" ]] && grub_lines+=(" initrd /boot/initrd") grub_lines+=("}") printf '%s\n' "${grub_lines[@]}" > "${iso_root}/boot/grub/grub.cfg" # ── Step 7: Create a README on the ISO ─────────────────────────────── cat > "${iso_root}/README.txt" < Inspect: qcrows-inspect README # ── Step 8: Build ISO ──────────────────────────────────────────────── log "Building ISO image..." if [[ "$EFI_PARTITION" == "true" ]]; then log "Creating EFI-bootable ISO with grub-mkrescue..." grub-mkrescue -o "$output" "$iso_root" 2>/dev/null || die "grub-mkrescue failed" else # MBR-bootable ISO using isolinux + xorriso/genisoimage log "Creating MBR-bootable ISO..." local isolinux_bin isolinux_bin=$(candidate_file \ /usr/lib/ISOLINUX/isolinux.bin \ /usr/share/syslinux/isolinux.bin \ /usr/lib/syslinux/isolinux.bin \ || true) [[ -z "$isolinux_bin" ]] && die "isolinux.bin not found — install syslinux or isolinux" # Copy isolinux binaries cp "$isolinux_bin" "${iso_root}/boot/isolinux/isolinux.bin" # Copy syslinux modules local syslinux_dir syslinux_dir=$(dirname "$isolinux_bin") find "$syslinux_dir" -name '*.c32' -exec cp {} "${iso_root}/boot/isolinux/" \; 2>/dev/null || true case "$ISO_TOOL" in xorriso) local isohdpfx isohdpfx=$(candidate_file "${syslinux_dir}/isohdpfx.bin" || true) local -a xorriso_args=( -as mkisofs -o "$output" -c boot/isolinux/boot.cat -b boot/isolinux/isolinux.bin -no-emul-boot -boot-load-size 4 -boot-info-table -V "${BOOT_LABEL}" -J -R ) [[ -n "$isohdpfx" ]] && xorriso_args=(-isohybrid-mbr "$isohdpfx" "${xorriso_args[@]}") xorriso "${xorriso_args[@]}" "$iso_root" 2>/dev/null || die "xorriso ISO creation failed" ;; genisoimage|mkisofs) "${ISO_TOOL}" \ -o "$output" \ -c boot/isolinux/boot.cat \ -b boot/isolinux/isolinux.bin \ -no-emul-boot \ -boot-load-size 4 \ -boot-info-table \ -V "${BOOT_LABEL}" \ -J -R \ "$iso_root" 2>/dev/null || die "${ISO_TOOL} ISO creation failed" # Make hybrid (dd-able to USB) command -v isohybrid &>/dev/null && isohybrid "$output" 2>/dev/null \ || warn "isohybrid failed — ISO may not be USB-bootable" ;; esac fi local final_size final_size=$(du -sh "$output" | cut -f1) # ── Summary ────────────────────────────────────────────────────────── echo "" echo -e "${GREEN}╔══════════════════════════════════════════════╗${NC}" echo -e "${GREEN}║ QCrows → ISO export complete ║${NC}" echo -e "${GREEN}╚══════════════════════════════════════════════╝${NC}" echo "" echo -e " Output: ${CYAN}${output}${NC}" echo -e " Size: ${CYAN}${final_size}${NC}" echo -e " Kernel: ${CYAN}${KERNEL_VERSION}${NC}" echo -e " Boot mode: ${CYAN}$([ "$EFI_PARTITION" == "true" ] && echo "EFI/GRUB" || echo "MBR/isolinux")${NC}" echo -e " Hybrid: ${CYAN}$([ "$EFI_PARTITION" == "false" ] && echo "yes (isohybrid)" || echo "EFI only")${NC}" echo "" echo -e " Write to USB:" echo -e " ${CYAN}dd if=${output} of=/dev/sdX bs=4M status=progress && sync${NC}" echo "" echo -e " Boot with QEMU:" echo -e " ${CYAN}qemu-system-x86_64 -m 1G -smp 2 -cdrom ${output} -enable-kvm${NC}" echo "" echo -e " Kata files on ISO:" echo -e " ${CYAN}/kata/vmlinuz${NC} — Guest kernel" echo -e " ${CYAN}/kata/rootfs.tar.gz${NC} — Guest rootfs" echo -e " ${CYAN}/kata/initrd${NC} — Initial ramdisk" echo -e " ${CYAN}/kata/metadata.toml${NC} — Image manifest" echo -e " ${CYAN}/kata/menu.toml${NC} — Cockpit UI entry" echo "" } # ─── PXE Export ───────────────────────────────────────────────────────────── export_pxe() { local archive="$1" local tftp_root="$2" log "Exporting QCrows image to PXE/TFTP directory" # Prerequisite checks local -a missing=() command -v mksquashfs &>/dev/null || missing+=(mksquashfs) command -v dnsmasq &>/dev/null || missing+=(dnsmasq) [[ ${#missing[@]} -gt 0 ]] && warn "Missing tools: ${missing[*]} — PXE boot may not function until installed" # Extract archive to temp directory TMPDIR=$(mktemp -d "${TMPDIR:-/tmp}/qcrows-export-pxe.XXXXXX") extract_qcrows "$archive" "$TMPDIR" # Derive image slug from metadata local image_name image_name=$(awk '/^name/{print $3}' "${TMPDIR}/metadata.toml" | tr -d '"' | tr ' ' '-' || echo "kata-image") [[ -z "$image_name" ]] && image_name="kata-image" local pxe_dir="${tftp_root}/kata/${image_name}" local pxe_cfg_dir="${tftp_root}/pxelinux.cfg" log "Creating PXE directory structure: ${pxe_dir}" mkdir -p "$pxe_dir" "$pxe_cfg_dir" # Copy kernel to TFTP log "Copying kernel to ${pxe_dir}/vmlinuz" cp "$KERNEL_FILE" "${pxe_dir}/vmlinuz" # Copy initrd if present local initrd_dest="" if [[ -n "$INITRD_FILE" ]]; then log "Copying initrd to ${pxe_dir}/initrd.img" cp "$INITRD_FILE" "${pxe_dir}/initrd.img" initrd_dest="kata/${image_name}/initrd.img" fi # Pack rootfs as squashfs for efficient network boot local rootfs_dest="${pxe_dir}/rootfs.squashfs" if command -v mksquashfs &>/dev/null; then log "Packing rootfs as squashfs to ${rootfs_dest}" local rootfs_unpack_dir rootfs_unpack_dir=$(mktemp -d "${TMPDIR}/rootfs-unpack.XXXXXX") tar xf "$ROOTFS_FILE" -C "$rootfs_unpack_dir" 2>/dev/null || { warn "Rootfs extraction failed — copying raw archive instead" cp "$ROOTFS_FILE" "${pxe_dir}/rootfs.tar.gz" rootfs_dest="${pxe_dir}/rootfs.tar.gz" } if [[ -d "$rootfs_unpack_dir" && ! -f "${pxe_dir}/rootfs.tar.gz" ]]; then mksquashfs "$rootfs_unpack_dir" "$rootfs_dest" -noappend -comp zstd -Xcompression-level 3 2>/dev/null \ || mksquashfs "$rootfs_unpack_dir" "$rootfs_dest" -noappend -comp gzip 2>/dev/null \ || { warn "Squashfs creation failed — using raw rootfs"; cp "$ROOTFS_FILE" "${pxe_dir}/rootfs.tar.gz"; rootfs_dest="${pxe_dir}/rootfs.tar.gz"; } rm -rf "$rootfs_unpack_dir" fi else warn "mksquashfs not available — copying raw rootfs archive" cp "$ROOTFS_FILE" "${pxe_dir}/rootfs.tar.gz" rootfs_dest="${pxe_dir}/rootfs.tar.gz" fi # Build kernel command line local base_cmdline="root=/dev/nfs nfsroot=${PXE_SERVER_IP}:${pxe_dir} ip=dhcp console=ttyS0,115200" local cmdline cmdline=$(build_cmdline "$base_cmdline" "$BOOT_PARAMS_FILE") [[ -n "$PXE_APPEND_CMDLINE" ]] && cmdline="${cmdline} ${PXE_APPEND_CMDLINE}" # Generate pxelinux configuration local pxe_config_file="${pxe_cfg_dir}/${image_name}" log "Writing PXE configuration to ${pxe_config_file}" { [[ "$PXE_DEFAULT" == "true" ]] && echo "DEFAULT ${image_name}" echo "LABEL ${image_name}" echo " MENU LABEL Kata ${image_name}" echo " KERNEL kata/${image_name}/vmlinuz" [[ -n "$initrd_dest" ]] && echo " INITRD ${initrd_dest}" echo " APPEND ${cmdline}" } > "$pxe_config_file" # Update dnsmasq configuration local dnsmasq_conf="/etc/dnsmasq.d/kata-pxe.conf" if [[ -d "/etc/dnsmasq.d" ]]; then log "Updating dnsmasq configuration at ${dnsmasq_conf}" if [[ ! -f "$dnsmasq_conf" ]] || ! grep -q "pxe-service" "$dnsmasq_conf" 2>/dev/null; then { echo "" echo "# Kata Containers PXE — managed by qcrows-export" echo "dhcp-range=192.168.1.100,192.168.1.200,255.255.255.0,12h" echo "dhcp-boot=pxelinux.0" echo "pxe-service=x86PC,\"Kata QCrows Boot\",pxelinux" echo "enable-tftp" echo "tftp-root=${tftp_root}" echo "" } >> "$dnsmasq_conf" fi log "dnsmasq config updated — restart dnsmasq to activate" else warn "/etc/dnsmasq.d not found — configure dnsmasq manually for DHCP+TFTP" fi # ── Summary ────────────────────────────────────────────────────────── echo "" echo -e "${GREEN}╔══════════════════════════════════════════════╗${NC}" echo -e "${GREEN}║ QCrows → PXE export complete ║${NC}" echo -e "${GREEN}╚══════════════════════════════════════════════╝${NC}" echo "" echo -e " TFTP Root: ${CYAN}${tftp_root}${NC}" echo -e " Image Dir: ${CYAN}${pxe_dir}${NC}" echo -e " Kernel: ${CYAN}${pxe_dir}/vmlinuz${NC}" [[ -n "$initrd_dest" ]] && echo -e " Initrd: ${CYAN}${pxe_dir}/initrd.img${NC}" echo -e " Rootfs: ${CYAN}${rootfs_dest}${NC}" echo -e " PXE Config: ${CYAN}${pxe_config_file}${NC}" echo -e " Server IP: ${CYAN}${PXE_SERVER_IP}${NC}" echo -e " Default Entry: ${CYAN}$([ "$PXE_DEFAULT" == "true" ] && echo "yes" || echo "no")${NC}" echo "" echo -e " Next steps:" echo -e " ${CYAN}1. Ensure pxelinux.0 exists in ${tftp_root}/${NC}" echo -e " ${CYAN}2. Restart dnsmasq: systemctl restart dnsmasq${NC}" echo -e " ${CYAN}3. Boot target machines via PXE network boot${NC}" echo "" } # ─── Entry Point ──────────────────────────────────────────────────────────── parse_args "$@" case "$FORMAT" in qcow2) export_qcow2 "$INPUT" "$OUTPUT" ;; iso) export_iso "$INPUT" "$OUTPUT" ;; pxe) export_pxe "$INPUT" "$OUTPUT" ;; *) die "Unhandled format: ${FORMAT}" ;; esac