Skip to content

Building Baremetal Images with Disk Image Builder

This guide covers building Ubuntu QCOW2 images for baremetal provisioning using diskimage-builder (DIB), suitable for deployment with Metal3/Ironic.


Prerequisites

Common Dependencies (amd64 & arm64)

pip3 install diskimage-builder
sudo apt update
sudo apt install -y qemu-utils

Additional Dependencies (arm64 only)

sudo apt install -y kpartx gdisk

Step 1: Build the Base Image

Create a script called create_image.sh with the following content:

#!/usr/bin/env bash
set -euo pipefail

# Ubuntu release codename
export DIB_RELEASE="noble"
export DIB_CLOUD_INIT_DATASOURCES="ConfigDrive, OpenStack"

# Output settings
OUTPUT_IMAGE_NAME="ubuntu-24.04-baremetal-final"
OUTPUT_FORMAT="qcow2"

# Elements for a UEFI-bootable, bare-metal-ready image
ELEMENTS="ubuntu vm baremetal cloud-init openssh-server \
          block-device-efi bootloader grub2 \
          growroot \
          dracut-network dhcp-all-interfaces"

# Additional packages
PACKAGES="${PACKAGES:-} lvm2 mdadm smartmontools"

# Build the image
disk-image-create \
  -o "${OUTPUT_IMAGE_NAME}.${OUTPUT_FORMAT}" \
  -t "${OUTPUT_FORMAT}" \
  ${ELEMENTS} \
  -p "${PACKAGES}" \
  --checksum

Run the script:

chmod +x create_image.sh
./create_image.sh

This produces ubuntu-24.04-baremetal-final.qcow2.


Step 2: Resize ESP Partition and Fix GRUB

After building the base image, the EFI System Partition (ESP) may need resizing and GRUB needs to be properly configured. The script below handles both.

Create a script called finalize_image.sh:

#!/usr/bin/env bash
set -euo pipefail

############################################
# CONFIG
############################################
ORIGINAL_IMAGE="${1:-ubuntu-24.04-arm-baremetal.qcow2}"
WORK_IMAGE="working.qcow2"
RESIZED_IMAGE="resized.qcow2"
FINAL_IMAGE="ubuntu-24.04-arm-baremetal-final.qcow2"
MOUNT_DIR="/mnt/qcow2_image"
NBD_DEV="/dev/nbd0"
FORCE="${FORCE:-false}"

############################################
# CLEANUP HANDLER
############################################
cleanup() {
    echo "Cleaning up..."
    set +e
    mountpoint -q "$MOUNT_DIR/boot/efi" && sudo umount "$MOUNT_DIR/boot/efi"
    mountpoint -q "$MOUNT_DIR/dev"      && sudo umount "$MOUNT_DIR/dev"
    mountpoint -q "$MOUNT_DIR/proc"     && sudo umount "$MOUNT_DIR/proc"
    mountpoint -q "$MOUNT_DIR/sys"      && sudo umount "$MOUNT_DIR/sys"
    mountpoint -q "$MOUNT_DIR"          && sudo umount "$MOUNT_DIR"
    [ -b "$NBD_DEV" ] && sudo qemu-nbd -d "$NBD_DEV" 2>/dev/null || true
    echo "Cleanup complete"
}
trap cleanup EXIT

############################################
# FORCE REBUILD (optional)
############################################
if [ "$FORCE" = "true" ]; then
    echo "FORCE rebuild enabled. Removing old artifacts..."
    rm -f "$WORK_IMAGE" "$RESIZED_IMAGE" "$FINAL_IMAGE"
fi

[ -f "$ORIGINAL_IMAGE" ] || { echo "Image not found: $ORIGINAL_IMAGE"; exit 1; }

############################################
# INSTALL REQUIRED TOOLS
############################################
sudo apt-get update
sudo apt-get install -y libguestfs-tools qemu-utils

############################################
# INSPECT ORIGINAL IMAGE
############################################
echo "Inspecting original image..."
virt-filesystems --long --parts --filesystems -a "$ORIGINAL_IMAGE"
qemu-img info "$ORIGINAL_IMAGE"

############################################
# AUTO-DETECT EFI + ROOT PARTITIONS
############################################
EFI_PART=$(virt-filesystems -a "$ORIGINAL_IMAGE" --long --filesystems | \
           awk '$3 == "vfat" {print $1; exit}')

ROOT_PART=$(virt-filesystems -a "$ORIGINAL_IMAGE" --long --filesystems | \
            awk '$3 ~ /ext4|xfs/ {print $1, $6}' | \
            sort -k2 -nr | head -1 | awk '{print $1}')

[ -n "$EFI_PART" ]  || { echo "EFI partition not found"; exit 1; }
[ -n "$ROOT_PART" ] || { echo "Root partition not found"; exit 1; }

echo "EFI Partition: $EFI_PART"
echo "Root Partition: $ROOT_PART"

############################################
# RESIZE + SPARSIFY
############################################
if [ -f "$FINAL_IMAGE" ]; then
    echo "Final image already exists. Skipping resize & sparsify."
elif [ -f "$RESIZED_IMAGE" ]; then
    echo "Resized image exists. Running sparsify only..."
    virt-sparsify --compress "$RESIZED_IMAGE" "$FINAL_IMAGE"
else
    cp "$ORIGINAL_IMAGE" "$WORK_IMAGE"

    NEW_SIZE=5G
    qemu-img create -f qcow2 "$RESIZED_IMAGE" "$NEW_SIZE"

    virt-resize \
      --resize-force "$EFI_PART=300M" \
      --expand "$ROOT_PART" \
      "$WORK_IMAGE" \
      "$RESIZED_IMAGE"

    virt-sparsify --compress "$RESIZED_IMAGE" "$FINAL_IMAGE"
fi

############################################
# FIX GRUB
############################################
echo "Fixing GRUB..."

sudo modprobe nbd max_part=16
sudo qemu-nbd -c "$NBD_DEV" "$FINAL_IMAGE"
sleep 2

EFI_DEV=""
ROOT_DEV=""
while read -r name fstype size; do
    [[ "$fstype" == "vfat" ]] && EFI_DEV="/dev/$name"
    [[ "$fstype" == "ext4" || "$fstype" == "xfs" ]] && ROOT_DEV="/dev/$name"
done < <(lsblk -ln -o NAME,FSTYPE,SIZE "$NBD_DEV")

[ -n "$EFI_DEV" ]  || { echo "EFI device not found"; exit 1; }
[ -n "$ROOT_DEV" ] || { echo "Root device not found"; exit 1; }

sudo mkdir -p "$MOUNT_DIR"
sudo mount "$ROOT_DEV" "$MOUNT_DIR"
sudo mkdir -p "$MOUNT_DIR/boot/efi"
sudo mount "$EFI_DEV" "$MOUNT_DIR/boot/efi"
sudo mount --bind /dev  "$MOUNT_DIR/dev"
sudo mount --bind /proc "$MOUNT_DIR/proc"
sudo mount --bind /sys  "$MOUNT_DIR/sys"

# Detect image architecture
IMAGE_ARCH=$(sudo chroot "$MOUNT_DIR" uname -m || echo "")
if [[ "$IMAGE_ARCH" == "x86_64" ]]; then
    GRUB_TARGET="x86_64-efi"
elif [[ "$IMAGE_ARCH" == "aarch64" || "$IMAGE_ARCH" == "arm64" ]]; then
    GRUB_TARGET="arm64-efi"
else
    echo "Unsupported architecture: $IMAGE_ARCH"; exit 1
fi

# Verify GRUB binaries exist
[ -d "$MOUNT_DIR/usr/lib/grub/$GRUB_TARGET" ] || \
    { echo "GRUB EFI binaries not found"; exit 1; }

# Install GRUB
sudo chroot "$MOUNT_DIR" grub-install \
    --target="$GRUB_TARGET" \
    --efi-directory=/boot/efi \
    --bootloader-id=ubuntu \
    --recheck

sudo chroot "$MOUNT_DIR" grub-install \
    --target="$GRUB_TARGET" \
    --efi-directory=/boot/efi \
    --bootloader-id=BOOT \
    --recheck

sudo chroot "$MOUNT_DIR" update-grub

echo "GRUB fixed successfully"

############################################
# GENERATE CHECKSUM
############################################
sha256sum "$FINAL_IMAGE" > "$FINAL_IMAGE.sha256"
sha256sum --check "$FINAL_IMAGE.sha256"

echo "FINAL IMAGE READY: $FINAL_IMAGE"

Run the script:

chmod +x finalize_image.sh
./finalize_image.sh ubuntu-24.04-baremetal-final.qcow2

To force a full rebuild from scratch:

FORCE=true ./finalize_image.sh ubuntu-24.04-baremetal-final.qcow2

Troubleshooting: GRUB Drops to Prompt After Deployment

Problem

After deploying the image via Metal3/Ironic, GRUB drops to a shell prompt instead of booting automatically. You have to manually enter:

configfile (hd0,gpt1)/EFI/BOOT/grub.cfg

Root Cause

The GRUB bootloader inside the QCOW2 image is not configured to automatically locate its grub.cfg when booted on the target hardware.

Manual Fix

If you need to fix this on an already-built image without rerunning the full script, you can mount and chroot into it manually.

Mount the image:

sudo modprobe nbd max_part=16
sudo qemu-nbd -p 1 -c /dev/nbd0 ubuntu-24.04-baremetal-final.qcow2

# Verify partition layout
lsblk /dev/nbd0
fdisk -l /dev/nbd0

# Mount root and EFI partitions
sudo mkdir -p /mnt/qcow2_image
sudo mount /dev/nbd0p3 /mnt/qcow2_image
sudo mkdir -p /mnt/qcow2_image/boot/efi
sudo mount /dev/nbd0p1 /mnt/qcow2_image/boot/efi

# Bind-mount host filesystems
sudo mount --bind /dev  /mnt/qcow2_image/dev
sudo mount --bind /proc /mnt/qcow2_image/proc
sudo mount --bind /sys  /mnt/qcow2_image/sys

# Copy DNS resolver config
sudo rm -f /mnt/qcow2_image/etc/resolv.conf
sudo cp /etc/resolv.conf /mnt/qcow2_image/etc/resolv.conf

# Enter the image
sudo chroot /mnt/qcow2_image /bin/bash

Inside the chroot — reinstall GRUB:

apt update
apt install -y grub-efi-amd64  # for x86_64 UEFI systems

grub-install --target=x86_64-efi \
    --efi-directory=/boot/efi \
    --bootloader-id=ubuntu \
    --recheck

grub-install --target=x86_64-efi \
    --efi-directory=/boot/efi \
    --bootloader-id=BOOT \
    --recheck

update-grub
exit

Note

For arm64 images, replace grub-efi-amd64 with grub-efi-arm64 and use --target=arm64-efi.

Unmount and disconnect:

sudo umount /mnt/qcow2_image/boot/efi
sudo umount /mnt/qcow2_image/dev
sudo umount /mnt/qcow2_image/proc
sudo umount /mnt/qcow2_image/sys
sudo umount /mnt/qcow2_image
sudo qemu-nbd -d /dev/nbd0

Generate checksum:

sha256sum ubuntu-24.04-baremetal-final.qcow2 > ubuntu-24.04-baremetal-final.qcow2.sha256
sha256sum --check ubuntu-24.04-baremetal-final.qcow2.sha256

Final Output

After completing the steps above, the image ubuntu-24.04-baremetal-final.qcow2 is ready for use with your Metal3/Ironic baremetal provisioning setup.