Some checks failed
particle-os CI / Test particle-os (push) Failing after 1s
particle-os CI / Integration Test (push) Has been skipped
particle-os CI / Security & Quality (push) Failing after 1s
Tests / test (1.21.x) (push) Failing after 1s
Tests / test (1.22.x) (push) Failing after 1s
particle-os CI / Build and Release (push) Has been skipped
432 lines
13 KiB
Bash
Executable file
432 lines
13 KiB
Bash
Executable file
#!/bin/bash
|
|
|
|
# deb-bootc-image-builder - Simplified container to disk image converter
|
|
# This is a simplified fork of the original Fedora bootc-image-builder
|
|
# that uses native Debian tools instead of osbuild
|
|
|
|
set -euo pipefail
|
|
|
|
# System tool paths
|
|
QEMU_IMG="/usr/bin/qemu-img"
|
|
RSYNC="/usr/bin/rsync"
|
|
TAR="/bin/tar"
|
|
MKDIR="/bin/mkdir"
|
|
CP="/bin/cp"
|
|
LOSETUP="/usr/sbin/losetup"
|
|
PARTED="/usr/sbin/parted"
|
|
MOUNT="/bin/mount"
|
|
UMOUNT="/bin/umount"
|
|
BOOTUPD="/usr/libexec/bootupd"
|
|
GRUB_INSTALL="/usr/sbin/grub-install"
|
|
GRUB_MKCONFIG="/usr/sbin/grub-mkconfig"
|
|
|
|
# Default values
|
|
CONTAINER_IMAGE=""
|
|
OUTPUT_FORMAT="qcow2"
|
|
OUTPUT_DIR="/output"
|
|
STORE_DIR="/store"
|
|
CONTAINER_STORAGE="/var/lib/containers/storage"
|
|
|
|
# Colors for output
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
BLUE='\033[0;34m'
|
|
NC='\033[0m' # No Color
|
|
|
|
# Global variables for cleanup
|
|
WORK_DIR=""
|
|
LOOP_DEV=""
|
|
MOUNT_POINT=""
|
|
CONTAINER_FS_DIR=""
|
|
DISK_RAW=""
|
|
|
|
# Logging functions
|
|
log_info() {
|
|
echo -e "${GREEN}[INFO]${NC} $1"
|
|
}
|
|
|
|
log_warn() {
|
|
echo -e "${YELLOW}[WARN]${NC} $1"
|
|
}
|
|
|
|
log_error() {
|
|
echo -e "${RED}[ERROR]${NC} $1"
|
|
}
|
|
|
|
# Help function
|
|
show_help() {
|
|
cat << EOF
|
|
Simplified Debian bootc-image-builder
|
|
|
|
Usage: $0 [OPTIONS] <container-image>
|
|
|
|
Options:
|
|
-f, --format FORMAT Output format (qcow2, raw, img) [default: qcow2]
|
|
-o, --output DIR Output directory [default: /output]
|
|
-s, --store DIR Store directory [default: /store]
|
|
-h, --help Show this help message
|
|
|
|
Examples:
|
|
$0 simple-cli:latest
|
|
$0 -f raw -o /tmp/output simple-cli:latest
|
|
$0 --format qcow2 --output /output simple-cli:latest
|
|
|
|
EOF
|
|
}
|
|
|
|
# Parse command line arguments
|
|
while [[ $# -gt 0 ]]; do
|
|
case $1 in
|
|
-f|--format)
|
|
OUTPUT_FORMAT="$2"
|
|
shift 2
|
|
;;
|
|
-o|--output)
|
|
OUTPUT_DIR="$2"
|
|
shift 2
|
|
;;
|
|
-s|--store)
|
|
STORE_DIR="$2"
|
|
shift 2
|
|
;;
|
|
-h|--help)
|
|
show_help
|
|
exit 0
|
|
;;
|
|
-*)
|
|
log_error "Unknown option $1"
|
|
show_help
|
|
exit 1
|
|
;;
|
|
*)
|
|
CONTAINER_IMAGE="$1"
|
|
shift
|
|
;;
|
|
esac
|
|
done
|
|
|
|
# Validate inputs
|
|
if [[ -z "$CONTAINER_IMAGE" ]]; then
|
|
log_error "Container image is required"
|
|
show_help
|
|
exit 1
|
|
fi
|
|
|
|
if [[ ! -d "$OUTPUT_DIR" ]]; then
|
|
log_error "Output directory $OUTPUT_DIR does not exist"
|
|
exit 1
|
|
fi
|
|
|
|
# Validate output format
|
|
case "$OUTPUT_FORMAT" in
|
|
qcow2|raw|img)
|
|
;;
|
|
*)
|
|
log_error "Unsupported output format: $OUTPUT_FORMAT"
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
log_info "Starting simplified bootc-image-builder"
|
|
log_info "Container image: $CONTAINER_IMAGE"
|
|
log_info "Output format: $OUTPUT_FORMAT"
|
|
log_info "Output directory: $OUTPUT_DIR"
|
|
|
|
# Create working directory in project directory
|
|
WORK_DIR="$(pwd)/work/bootc-builder-$(date +%s)"
|
|
mkdir -p "$WORK_DIR" || {
|
|
log_error "Failed to create working directory"
|
|
exit 1
|
|
}
|
|
log_info "Working directory: $WORK_DIR"
|
|
|
|
# Check available space
|
|
AVAILABLE_SPACE=$(df "$WORK_DIR" | awk 'NR==2 {print $4}')
|
|
if [[ "$AVAILABLE_SPACE" -lt 5000000 ]]; then # Less than 5GB
|
|
log_warn "Low disk space available: ${AVAILABLE_SPACE}KB"
|
|
log_warn "This might cause issues with large containers"
|
|
fi
|
|
|
|
# Signal handling for cleanup
|
|
cleanup_on_exit() {
|
|
local exit_code=$?
|
|
log_info "Cleaning up resources..."
|
|
|
|
# Unmount if mounted
|
|
if [[ -n "$MOUNT_POINT" && -d "$MOUNT_POINT" ]]; then
|
|
if mountpoint -q "$MOUNT_POINT" 2>/dev/null; then
|
|
log_info "Unmounting $MOUNT_POINT..."
|
|
sudo umount "$MOUNT_POINT" 2>/dev/null || true
|
|
fi
|
|
fi
|
|
|
|
# Detach loop device if exists
|
|
if [[ -n "$LOOP_DEV" && -b "$LOOP_DEV" ]]; then
|
|
log_info "Detaching loop device $LOOP_DEV..."
|
|
sudo losetup -d "$LOOP_DEV" 2>/dev/null || true
|
|
fi
|
|
|
|
# Clean up any remaining loop devices that might be ours
|
|
if [[ -n "$WORK_DIR" ]]; then
|
|
log_info "Checking for orphaned loop devices..."
|
|
for loop_dev in /dev/loop*; do
|
|
if [[ -b "$loop_dev" ]]; then
|
|
local mount_point
|
|
mount_point=$(losetup -l "$loop_dev" 2>/dev/null | grep -o ".*/work/bootc-builder-[^[:space:]]*" || true)
|
|
if [[ -n "$mount_point" ]]; then
|
|
log_info "Detaching orphaned loop device $loop_dev..."
|
|
sudo losetup -d "$loop_dev" 2>/dev/null || true
|
|
fi
|
|
fi
|
|
done
|
|
fi
|
|
|
|
# Remove working directory
|
|
if [[ -n "$WORK_DIR" && -d "$WORK_DIR" ]]; then
|
|
log_info "Removing working directory $WORK_DIR..."
|
|
sudo rm -rf "$WORK_DIR" 2>/dev/null || true
|
|
fi
|
|
|
|
# Remove any remaining temporary files
|
|
if [[ -n "$DISK_RAW" && -f "$DISK_RAW" ]]; then
|
|
log_info "Removing temporary disk image $DISK_RAW..."
|
|
sudo rm -f "$DISK_RAW" 2>/dev/null || true
|
|
fi
|
|
|
|
if [[ -n "$CONTAINER_FS_DIR" && -d "$CONTAINER_FS_DIR" ]]; then
|
|
log_info "Removing container filesystem directory $CONTAINER_FS_DIR..."
|
|
sudo rm -rf "$CONTAINER_FS_DIR" 2>/dev/null || true
|
|
fi
|
|
|
|
log_info "Cleanup completed"
|
|
exit "$exit_code"
|
|
}
|
|
|
|
# Set up signal handlers
|
|
trap cleanup_on_exit EXIT INT TERM
|
|
|
|
# Function to clean up any existing loop devices from previous runs
|
|
cleanup_existing_loop_devices() {
|
|
log_info "Checking for existing loop devices from previous runs..."
|
|
for loop_dev in /dev/loop*; do
|
|
if [[ -b "$loop_dev" ]]; then
|
|
local mount_point
|
|
mount_point=$(losetup -l "$loop_dev" 2>/dev/null | grep -o ".*/work/bootc-builder-[^[:space:]]*" || true)
|
|
if [[ -n "$mount_point" ]]; then
|
|
log_warn "Found existing loop device $loop_dev from previous run, cleaning up..."
|
|
# Try to unmount if mounted
|
|
if mountpoint -q "$mount_point" 2>/dev/null; then
|
|
sudo umount "$mount_point" 2>/dev/null || true
|
|
fi
|
|
# Detach the loop device
|
|
sudo losetup -d "$loop_dev" 2>/dev/null || true
|
|
# Remove the mount point directory
|
|
sudo rm -rf "$mount_point" 2>/dev/null || true
|
|
fi
|
|
fi
|
|
done
|
|
}
|
|
|
|
# Clean up any existing loop devices at startup
|
|
cleanup_existing_loop_devices
|
|
|
|
# Check if container image exists locally
|
|
log_info "Checking if container image exists locally: $CONTAINER_IMAGE"
|
|
if podman images --format "{{.Repository}}:{{.Tag}}" | grep -q "^$CONTAINER_IMAGE$"; then
|
|
log_info "Container image found locally, skipping pull"
|
|
elif podman images --format "{{.Repository}}:{{.Tag}}" | grep -q "simple-cli:latest"; then
|
|
log_info "Found simple-cli:latest locally, using that instead"
|
|
CONTAINER_IMAGE="simple-cli:latest"
|
|
elif podman images --format "{{.Repository}}:{{.Tag}}" | grep -q "localhost/simple-cli:latest"; then
|
|
log_info "Found localhost/simple-cli:latest locally, using that instead"
|
|
CONTAINER_IMAGE="localhost/simple-cli:latest"
|
|
else
|
|
log_info "Container image not found locally, attempting to pull: $CONTAINER_IMAGE"
|
|
podman pull "$CONTAINER_IMAGE" || {
|
|
log_error "Failed to pull container image. Please ensure the image is available."
|
|
log_error "You can either:"
|
|
log_error "1. Build the image locally first"
|
|
log_error "2. Use a fully qualified registry URL"
|
|
exit 1
|
|
}
|
|
fi
|
|
|
|
# Export the container to a tar file
|
|
log_info "Exporting container to tar..."
|
|
podman export "$(podman create --rm "$CONTAINER_IMAGE" true)" > "$WORK_DIR/container.tar"
|
|
|
|
# Create a loopback device and mount the container
|
|
log_info "Setting up loopback device..."
|
|
cd "$WORK_DIR"
|
|
|
|
# Extract the container
|
|
log_info "Extracting container filesystem..."
|
|
CONTAINER_FS_DIR="$WORK_DIR/container-fs"
|
|
mkdir -p "$CONTAINER_FS_DIR"
|
|
if ! tar -xf container.tar -C "$CONTAINER_FS_DIR"; then
|
|
log_error "Failed to extract container. This might be due to:"
|
|
log_error "1. Insufficient disk space"
|
|
log_error "2. Corrupted container archive"
|
|
log_error "3. Permission issues"
|
|
exit 1
|
|
fi
|
|
|
|
# Create disk image using qemu-img (no sudo required)
|
|
log_info "Creating disk image..."
|
|
IMAGE_SIZE="8G" # Increased size for enhanced simple-cli with Bazzite features
|
|
|
|
# Create a proper disk image with partitions for bootloader installation
|
|
log_info "Creating partitioned disk image for bootloader installation..."
|
|
|
|
# Create raw image file
|
|
DISK_RAW="$WORK_DIR/disk.raw"
|
|
$QEMU_IMG create -f raw "$DISK_RAW" "$IMAGE_SIZE"
|
|
|
|
# For bootloader installation, we need to create a proper disk structure
|
|
# This requires sudo for partitioning and mounting
|
|
log_info "Setting up disk partitions for bootloader installation..."
|
|
log_info "Note: This step requires sudo for disk operations"
|
|
|
|
# Create partition table and filesystem
|
|
log_info "Creating partition table and filesystem..."
|
|
sudo $PARTED "$DISK_RAW" mklabel msdos
|
|
sudo $PARTED "$DISK_RAW" mkpart primary ext4 1MiB 100%
|
|
sudo $PARTED "$DISK_RAW" set 1 boot on
|
|
|
|
# Setup loopback device
|
|
log_info "Setting up loopback device..."
|
|
LOOP_DEV=$(sudo $LOSETUP --find --show "$DISK_RAW")
|
|
log_info "Using loopback device: $LOOP_DEV"
|
|
|
|
# Force kernel to reread partition table
|
|
log_info "Rereading partition table..."
|
|
sudo partprobe "$LOOP_DEV" || true
|
|
|
|
# Get the partition device
|
|
PART_DEV="${LOOP_DEV}p1"
|
|
log_info "Partition device: $PART_DEV"
|
|
|
|
# Wait for partition to be available and verify it exists
|
|
log_info "Waiting for partition to be available..."
|
|
for i in {1..10}; do
|
|
if [[ -b "$PART_DEV" ]]; then
|
|
log_info "Partition device found: $PART_DEV"
|
|
break
|
|
fi
|
|
log_info "Waiting for partition device... (attempt $i/10)"
|
|
sleep 1
|
|
done
|
|
|
|
if [[ ! -b "$PART_DEV" ]]; then
|
|
log_error "Partition device not found: $PART_DEV"
|
|
log_error "Available devices:"
|
|
ls -la /dev/loop* || true
|
|
exit 1
|
|
fi
|
|
|
|
# Create filesystem
|
|
log_info "Creating ext4 filesystem..."
|
|
sudo mkfs.ext4 "$PART_DEV"
|
|
|
|
# Mount the partition
|
|
MOUNT_POINT="$WORK_DIR/mount-point"
|
|
mkdir -p "$MOUNT_POINT"
|
|
sudo $MOUNT "$PART_DEV" "$MOUNT_POINT"
|
|
|
|
# Copy container filesystem
|
|
log_info "Copying container filesystem to disk image..."
|
|
sudo rsync -a "$CONTAINER_FS_DIR/" "$MOUNT_POINT/"
|
|
|
|
# Install bootloader manually since deb-bootupd doesn't support non-default sysroots
|
|
log_info "Installing bootloader manually..."
|
|
if [[ -x "$GRUB_INSTALL" ]]; then
|
|
log_info "Found grub-install, installing GRUB bootloader..."
|
|
|
|
# Install GRUB to the mounted filesystem
|
|
# Use --root-directory to specify the mount point
|
|
# Use --target=i386-pc for BIOS systems
|
|
# Use --boot-directory to specify where boot files are located
|
|
log_info "Installing GRUB to $MOUNT_POINT..."
|
|
sudo "$GRUB_INSTALL" \
|
|
--root-directory="$MOUNT_POINT" \
|
|
--target=i386-pc \
|
|
--boot-directory="$MOUNT_POINT/boot" \
|
|
--force \
|
|
"$LOOP_DEV" || {
|
|
log_warn "GRUB installation failed, continuing without bootloader"
|
|
}
|
|
|
|
# Generate GRUB configuration manually since grub-mkconfig needs host environment
|
|
log_info "Generating GRUB configuration manually..."
|
|
|
|
# Find the actual kernel and initrd files in the container
|
|
KERNEL_FILE=$(find "$MOUNT_POINT/boot" -name "vmlinuz-*" | head -1)
|
|
INITRD_FILE=$(find "$MOUNT_POINT/boot" -name "initrd.img-*" | head -1)
|
|
|
|
if [[ -z "$KERNEL_FILE" ]] || [[ -z "$INITRD_FILE" ]]; then
|
|
log_error "Kernel or initrd not found in container"
|
|
log_error "Available files in /boot:"
|
|
ls -la "$MOUNT_POINT/boot/" || true
|
|
exit 1
|
|
fi
|
|
|
|
# Extract just the filename without the full path
|
|
KERNEL_NAME=$(basename "$KERNEL_FILE")
|
|
INITRD_NAME=$(basename "$INITRD_FILE")
|
|
|
|
log_info "Found kernel: $KERNEL_NAME"
|
|
log_info "Found initrd: $INITRD_NAME"
|
|
|
|
# Create a basic GRUB configuration for the container
|
|
cat > "$MOUNT_POINT/boot/grub/grub.cfg" << EOF
|
|
# GRUB configuration for simple-cli container
|
|
set timeout=1
|
|
set default=0
|
|
|
|
menuentry "Simple CLI" {
|
|
set root=(hd0,msdos1)
|
|
linux /boot/$KERNEL_NAME root=/dev/sda1 rw console=ttyS0 quiet splash fastboot
|
|
initrd /boot/$INITRD_NAME
|
|
}
|
|
|
|
menuentry "Simple CLI (Recovery)" {
|
|
set root=(hd0,msdos1)
|
|
linux /boot/$KERNEL_NAME root=/dev/sda1 rw single console=ttyS0
|
|
initrd /boot/$INITRD_NAME
|
|
}
|
|
EOF
|
|
|
|
log_info "GRUB configuration created at $MOUNT_POINT/boot/grub/grub.cfg"
|
|
|
|
else
|
|
log_warn "grub-install not found at $GRUB_INSTALL, skipping bootloader installation"
|
|
log_warn "Image will not be bootable without bootloader"
|
|
fi
|
|
|
|
# Unmount
|
|
sudo $UMOUNT "$MOUNT_POINT"
|
|
|
|
# Detach loopback device
|
|
sudo $LOSETUP -d "$LOOP_DEV"
|
|
|
|
# Convert to requested format
|
|
log_info "Converting to $OUTPUT_FORMAT format..."
|
|
case "$OUTPUT_FORMAT" in
|
|
qcow2)
|
|
$QEMU_IMG convert -f raw -O qcow2 "$DISK_RAW" "$OUTPUT_DIR/$(basename "$CONTAINER_IMAGE" | tr ':' '_').qcow2"
|
|
;;
|
|
raw)
|
|
cp "$DISK_RAW" "$OUTPUT_DIR/$(basename "$CONTAINER_IMAGE" | tr ':' '_').raw"
|
|
;;
|
|
img)
|
|
cp "$DISK_RAW" "$OUTPUT_DIR/$(basename "$CONTAINER_IMAGE" | tr ':' '_').img"
|
|
;;
|
|
esac
|
|
|
|
log_info "Image creation completed successfully!"
|
|
log_info "Output file: $OUTPUT_DIR/$(basename "$CONTAINER_IMAGE" | tr ':' '_').$OUTPUT_FORMAT"
|
|
|
|
# List output files
|
|
log_info "Output directory contents:"
|
|
ls -la "$OUTPUT_DIR"
|