PXE booting with QEMU and UEFI

This is a followup to the qemu with PXE boot post I wrote a few years back. In this post I will cover the creation of a live filesystem using livemedia-creator, and PXE booting it with qemu and UEFI. I am assuming that you have some familiarity with using Anaconda to install Fedora, CentOS, and Red Hat Enterprise Linux. As well as with my previous posts about livemedia-creator .

Building the image with livemedia-creator

The creation process is similar to creating live isos but instead of an iso we will create the live root filesystem. This is an ext4 filesystem that is compressed using squashfs, and it is the same as the rootfs used on the boot.iso and live isos.

You will need to have the lorax package installed, and you should be using a distribution that is the same as, or as close as possible, to the distribution you are targeting (eg. Fedora 38, RHEL 9, etc.). For this post I was running on a Fedora 37 system, and using a Fedora 38 boot.iso. You can use this same process on RHEL and CentOS, with the only difference being the specific kernel and initramfs versions.

livemedia-creator can use a boot.iso and qemu to do the actual installation, or use Anaconda running on the host. In these examples I’ll be using a boot.iso. If you want to run Anaconda directly, install the lorax-lmc-novirt package and pass --no-virt instead of --iso boot.iso.

The biggest difference is that when running with --no-virt mode you need to be on a UEFI system or VM for Anaconda to correctly setup UEFI booting.

Install the lorax-lmc-virt package to make sure all of the necessary requirement are installed:

sudo dnf install -y lorax lorax-lmc-virt

Creating the kickstart for livemedia-creator

Overall you can do anything in the kickstart that you would normally do, but since this is a compressed filesystem image there are a few restrictions:

  • Partitioning should be reqpart and a single part / --fstype="ext4" --size=4000 entry.
  • Make sure the dracut-live package is included in the %packages section
  • Remove VM specific files in %post like /etc/fstab, etc.
    rm -f /etc/fstab
    rm -f /var/lib/systemd/random-seed
    rm -f /etc/machine-id
    touch /etc/machine-id

You can start with the minimal.ks example kickstart that is found in /usr/share/doc/lorax/ and modify it to fit your needs. And the full kickstart documentation is here .

Creating the PXE boot live filesystem

Download the boot.iso for the distribution, eg. the Fedora 38 boot.iso can be found here.

Run livemedia-creator to build to root filesystem and example PXE menu entry:

sudo livemedia-creator --iso ./boot.iso --ks ./minimal.ks --virt-uefi --make-pxe-live --resultdir /var/tmp/pxe-live/ 

When this is finished you will have several files in the /var/tmp/pxe-live/ directory:

* A kernel
* An initramfs image
* A compressed root filesystem named live-rootfs.squashfs.img
* A raw filesystem image
* An example PXE config file when booting with isolinux

The kernel, initramfs, and compressed rootfs will be used in the following steps.

If you are an experienced PXE user you can skip the qemu setup and jump directly into the PXE boot menu settings.

A qemu UEFI PXE startup script

The qemu script from the qemu with PXE boot post was fairly simple. Adding UEFI support complicates things a bit. You need to setup the UEFI firmware flash, and make a copy of the OVMF_VARS file. You can also set it up to use secure boot if you use the OVMF_CODE.secboot.fd file, but in this example I am using the non-secure boot firmware:

cp /usr/share/OVMF/OVMF_VARS.fd /tmp/qemu-pxe-OVMF_VARS.fd
qemu-kvm -cpu host -accel kvm \
-machine q35,smm=on -global driver=cfi.pflash01,property=secure,value=on \
-drive file=/usr/share/OVMF/OVMF_CODE.fd,if=pflash,format=raw,unit=0,readonly=on \
-drive file=/tmp/qemu-pxe-OVMF_VARS.fd,if=pflash,format=raw,unit=1 \
-netdev user,id=net0,net=,tftp=$HOME/configs/tftp/,bootfile=BOOTX64.EFI \
-device virtio-net-pci,netdev=net0 \
-object rng-random,id=virtio-rng0,filename=/dev/urandom \
-device virtio-rng-pci,rng=virtio-rng0,id=rng0,bus=pcie.0,addr=0x9 \
-serial stdio -boot n $@

Put this into a file named something like run-qemu-pxe and set the executable bit on it. This will boot a qemu UEFI system using the tftp setup in $HOME/configs/tftp/. We will setup that directory next.

Setup the tftp directory for UEFI

When PXE booting UEFI you need a copy of several files, you can get them from the boot.iso used to create the root filesystem, or from the distribution repository’s EFI/BOOT directory . I’ll use the ones from the boot.iso, mounted on /mnt/iso/.

mkdir -p $HOME/configs/tftp
sudo mount boot.iso /mnt/iso/
sudo cp /mnt/iso/EFI/BOOT/BOOTX64.EFI $HOME/configs/tftp/
sudo cp /mnt/iso/EFI/BOOT/grubx64.efi $HOME/configs/tftp/
sudo chown $USER $HOME/configs/tftp/*

Make sure the user has permissions on these files. On the boot.iso they are restricted to root, so ownership and/or permissions need to be changed.

Prepare the PXE files

There are several different ways to load the root filesystem. One is to pull it from a webserver over http, another is to use a combined initramfs that has the rootfs appended to the regular initramfs. Both of these methods need to have enough RAM on the system to hold the filesystem, the kernel, and the initramfs in addition to enough RAM for normal system operation.

We will place these files in the $HOME/configs/tftp/live/ directory. Copy the kernel, initramfs, and live-rootfs.squashfs.img files from /var/tmp/pxe-live/. Then create a combined.img file. The name of the initramfs file will vary based on your kernel version and distribution.

mkdir -p $HOME/configs/tftp/live/
cd $HOME/configs/tftp/live/
sudo cp /var/tmp/pxe-live/* .
sudo chown $USER *
echo ./live-rootfs.squashfs.img | cpio -c --quiet -L -o > rootfs.cpio
cat initramfs-6.2.12-300.fc38.x86_64.img rootfs.cpio > combined.img

If you want to use http to fetch the rootfs, setup a server that points to this directory. It can be as simple as running python3 -m http.server from the directory and then using root=live:http://HOST-IP:8000/live-rootfs.squashfs.img as the location in grub.cfg.

Create a grub menu

The GRUB bootloader needs a configuration file in the same directory as BOOTX64.EFI, so switch back to $HOME/configs/tftp and create a grub.cfg menu file using the following example. This has two entries. The first, combined-rootfs will boot the root filesystem using the tftp server. The second will boot it using http and a webserver that you have setup to point to the $HOME/configs/tftp/live/ directory.

set timeout=60

menuentry 'combined-rootfs' {
    linuxefi /live/vmlinuz-6.2.12-300.fc38.x86_64 root=live:/live-rootfs.squashfs.img rd.live.image console=ttyS0 console=tty1
    initrdefi /live/combined.img

menuentry 'http-rootfs' {
    linuxefi /live/vmlinuz-6.2.12-300.fc38.x86_64 root=live:http://HTTP-SERVER/live-rootfs.squashfs.img rd.live.image console=ttyS0 console=tty1
    initrdefi /live/initramfs-6.2.12-300.fc38.x86_64.img


With that file in place you should now be able to boot using the run-qemu-uefi script, remembering to give it enough memory:

run-qemu-uefi -m 4096

Select the menu entry to boot and after a brief pause for it to load the initramfs you should see kernel messages flying by. If you get an error mentioning tftp.c that usually means it could not find the kernel or initramfs file(s). Usually because the version or release number in the grub.cfg file doesn’t match what is available in $HOME/configs/tftp.