874 lines
35 KiB
Bash
Executable File
874 lines
35 KiB
Bash
Executable File
#!/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 <<EOF
|
|
qcrows-export v${VERSION} — Export QCrows image to qcow2 or ISO
|
|
|
|
USAGE:
|
|
qcrows-export --format FORMAT INPUT -o OUTPUT [OPTIONS]
|
|
|
|
REQUIRED:
|
|
--format FORMAT Export format: qcow2 | iso | pxe
|
|
INPUT Path to .qcrows archive
|
|
-o, --output PATH Output file (qcow2/iso) or TFTP directory (pxe)
|
|
|
|
QCOW2 OPTIONS:
|
|
--disk-size SIZE Disk size (e.g., 2G, 500M). Default: rootfs size + 256M
|
|
--no-compress Do not compress the qcow2 image
|
|
--efi Create an EFI-compatible partition layout
|
|
|
|
ISO OPTIONS:
|
|
--boot-label LABEL Boot partition label (default: QCROWS)
|
|
--root-label LABEL Root filesystem label (default: ROOTFS)
|
|
--efi Create EFI-bootable ISO (requires grub-mkrescue)
|
|
|
|
PXE OPTIONS:
|
|
--server-ip IP PXE/DHCP server IP (required for pxe format)
|
|
--no-default Do not set as default PXE entry
|
|
--append-cmdline S Extra kernel command-line parameters
|
|
|
|
GENERAL OPTIONS:
|
|
-v, --verbose Verbose output
|
|
-h, --help Show this help
|
|
--version Show version
|
|
|
|
PREREQUISITES:
|
|
qcow2: qemu-img, parted, mkfs.ext4, mount, tar
|
|
iso: xorriso or genisoimage, isolinux (for MBR), grub-mkrescue (for EFI)
|
|
pxe: dnsmasq, squashfs-tools (mksquashfs)
|
|
|
|
EXAMPLES:
|
|
# Export to qcow2 (sized from rootfs)
|
|
qcrows-export --format qcow2 alpine-kata.qcrows -o alpine-kata.qcow2
|
|
|
|
# Export to 2GB qcow2 with EFI support
|
|
qcrows-export --format qcow2 --disk-size 2G --efi alpine-kata.qcrows -o alpine-efi.qcow2
|
|
|
|
# Export to bootable ISO for USB
|
|
qcrows-export --format iso alpine-kata.qcrows -o alpine-live.iso
|
|
|
|
# Write ISO to USB
|
|
dd if=alpine-live.iso of=/dev/sdX bs=4M status=progress && sync
|
|
|
|
# Export to PXE/TFTP directory
|
|
qcrows-export --format pxe --server-ip 192.168.1.1 alpine-kata.qcrows -o /srv/tftp
|
|
EOF
|
|
}
|
|
|
|
# ─── Parse Arguments ────────────────────────────────────────────────────────
|
|
parse_args() {
|
|
local positional_args=()
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--format) FORMAT="$2"; shift 2 ;;
|
|
--output|-o) OUTPUT="$2"; shift 2 ;;
|
|
--disk-size) DISK_SIZE="$2"; shift 2 ;;
|
|
--no-compress) COMPRESS_QCOW2=false; shift ;;
|
|
--efi) EFI_PARTITION=true; shift ;;
|
|
--boot-label) BOOT_LABEL="$2"; shift 2 ;;
|
|
--root-label) ROOT_LABEL="$2"; shift 2 ;;
|
|
--verbose|-v) VERBOSE=true; shift ;;
|
|
--server-ip) PXE_SERVER_IP="$2"; shift 2 ;;
|
|
--no-default) PXE_DEFAULT=false; shift ;;
|
|
--append-cmdline) PXE_APPEND_CMDLINE="$2"; shift 2 ;;
|
|
--help|-h) usage; exit 0 ;;
|
|
--version) echo "qcrows-export v${VERSION}"; exit 0 ;;
|
|
-*) die "Unknown option: $1 (use --help)" ;;
|
|
*) positional_args+=("$1"); shift ;;
|
|
esac
|
|
done
|
|
|
|
INPUT="${positional_args[0]:-}"
|
|
|
|
[[ -z "$FORMAT" ]] && die "Format is required (--format qcow2|iso|pxe)"
|
|
[[ -z "$INPUT" ]] && die "Input .qcrows file is required"
|
|
[[ -z "$OUTPUT" ]] && die "Output path is required (--output or -o)"
|
|
[[ ! -f "$INPUT" ]] && die "Input file not found: $INPUT"
|
|
|
|
case "$FORMAT" in
|
|
qcow2|iso|pxe) ;;
|
|
*) die "Unsupported format: ${FORMAT} (use qcow2, iso, or pxe)" ;;
|
|
esac
|
|
|
|
[[ "$FORMAT" == "pxe" && -z "$PXE_SERVER_IP" ]] && die "PXE format requires --server-ip"
|
|
}
|
|
|
|
# ─── Prerequisite Checks ───────────────────────────────────────────────────
|
|
check_prereqs_qcow2() {
|
|
local -a missing=()
|
|
command -v qemu-img &>/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" <<README
|
|
QCrows Live Image — $(date -u +%Y-%m-%d)
|
|
=========================================
|
|
Kernel: ${KERNEL_VERSION}
|
|
Format: QCrows live ISO
|
|
|
|
This ISO contains a Kata Container image usable in two modes:
|
|
|
|
1. LIVE BOOT: Boot this ISO directly (via USB or virtual CD-ROM).
|
|
The kernel and initrd load with the rootfs at /live/rootfs.tar.gz
|
|
|
|
2. KATA DEPLOY: Copy files from the /kata/ directory on this ISO
|
|
to /usr/share/kata-containers/ on your host system:
|
|
- vmlinuz (guest kernel)
|
|
- rootfs.tar.gz (guest filesystem)
|
|
- initrd (initial ramdisk)
|
|
- config (kernel .config)
|
|
- metadata.toml (image manifest)
|
|
- menu.toml (Cockpit UI entry)
|
|
- boot-params.conf (kernel parameters)
|
|
|
|
Then update your configuration.toml to point to the new paths.
|
|
|
|
Verify: qcrows-verify <archive.qcrows>
|
|
Inspect: qcrows-inspect <archive.qcrows>
|
|
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
|