Linux#

Use a supported Linux QEMU Copy-On-Write version 2 (QCOW2) image when one is available. Alternatively, install Linux in a virtual machine (VM) by using any hypervisor, then convert its disk to QCOW2.

The examples in this section use Ubuntu 24.04, but the image workflow is not specific to one Linux distribution. Prepare the image in a VM on a separate build host. Run the installation and validation commands inside that VM. Run qemu-img on the build host after you shut down the VM.

Create the Linux VM#

The generic image workflow is:

  1. On a build host, install a hypervisor and qemu-img.

  2. Verify the Linux installation ISO and create a VM with firmware, machine type, and disk bus supported by the target KubeVirt environment.

  3. Boot the VM from the ISO, install Linux on its system disk, apply updates, eject the ISO, and verify that the VM boots from the system disk.

  4. Install and validate the required guest software, then shut down the VM and convert its system disk to a standalone QCOW2 image.

The following example creates an Ubuntu 24.04 VM image.

Prepare the Ubuntu Build Host#

  1. Install Virtual Machine Manager, QEMU/KVM, libvirt, and the QEMU image tools:

    sudo apt update
    sudo apt install -y \
      virt-manager qemu-kvm qemu-utils ovmf \
      libvirt-daemon-system libvirt-clients
    
  2. Enable libvirt and grant the current user access:

    sudo systemctl enable --now libvirtd
    sudo adduser "$(id -un)" libvirt
    sudo adduser "$(id -un)" kvm
    

    Log out and log in again, then verify the system connection:

    ls -l /dev/kvm
    virsh -c qemu:///system list --all
    virt-host-validate qemu
    

Prepare the Ubuntu Installation Media#

  1. Create a build directory:

    mkdir -p "$HOME/vm-build"
    cd "$HOME/vm-build"
    
  2. Download the Ubuntu Server 24.04 ISO and its SHA256SUMS file from the Ubuntu release server.

  3. Verify the downloaded ISO:

    sha256sum -c SHA256SUMS --ignore-missing
    
  4. Copy the verified ISO into libvirt storage:

    sudo install -m 0644 <ubuntu-server-24.04.iso> /var/lib/libvirt/images/ubuntu-server-24.04.iso
    virsh -c qemu:///system pool-refresh default
    

Create the Initial Ubuntu VM#

  1. Start Virtual Machine Manager and select QEMU/KVM - System.

  2. Select File > New Virtual Machine > Local install media.

  3. Select Use ISO image > Browse, select the default storage pool and ubuntu-server-24.04.iso, then select Choose Volume. Select Ubuntu 24.04 manually if Virtual Machine Manager does not detect it.

  4. Configure 4 vCPUs and 8192 MiB of memory.

  5. Select Create a disk image for the virtual machine, set its capacity to 80 GiB, and continue.

  6. Name the VM ubuntu2404 and select Customize configuration before install.

  7. In Overview, select the Q35 chipset and UEFI x86_64: /usr/share/OVMF/OVMF_CODE_4M.fd (the UEFI firmware without Secure Boot). Set the system disk bus to VirtIO and the network interface device model to virtio.

  8. In Boot Options, place the installation CD-ROM before the system disk, then select Begin Installation.

  9. In the Ubuntu installer, install Ubuntu Server on the entire virtual disk, create an administrator account, and complete the installation.

  10. Reboot, detach the ISO, and confirm that the VM boots from the VirtIO disk. Apply updates and reboot again:

    sudo apt update
    sudo apt full-upgrade -y
    sudo reboot
    
  11. On the build host, record the source disk path for the export step:

    virsh -c qemu:///system domblklist ubuntu2404 --details
    

    The example export command below assumes that this command reports /var/lib/libvirt/images/ubuntu2404.qcow2. Use the reported path if it differs.

Attach Optional PCI Devices#

To validate GPU- or NIC-dependent software in the build VM, first configure the build host for input-output memory management unit (IOMMU) and Virtual Function I/O (VFIO) passthrough. Then complete the following steps:

  1. Shut down ubuntu2404 and open its hardware details in Virtual Machine Manager.

  2. Select Add Hardware > PCI Host Device and add the NVIDIA GPU. Add every required function of a multifunction device.

  3. Repeat the step for the ConnectX NIC or Single Root I/O Virtualization (SR-IOV) virtual function (VF). Do not select the build host’s management interface.

  4. Start the VM and verify the devices in Ubuntu:

    lspci -nn | grep -Ei 'NVIDIA|Mellanox'
    ip link
    

The following sections describe how to install and validate the required software inside the VM.

Install the Guest Components#

This workflow requires cloud-init to install and enable the network initialization files at first boot. qemu-guest-agent is optional. It lets KubeVirt query guest information and perform supported guest-management operations. Install it when the cluster configuration requires it.

Install the following optional components as the application requires them:

Component

Required When

Installation Source

NVIDIA GPU driver

The workload uses a GPU passed through to the VM

NVIDIA GPU Driver Installation Guide

DOCA-OFED or MLNX_OFED

The workload uses a ConnectX NIC or BlueField data processing unit (DPU)

NVIDIA DOCA Installation Guide for Linux

CUDA Toolkit

The application uses CUDA

CUDA Installation Guide for Linux

linuxptp

The workload requires Precision Time Protocol (PTP) synchronization, including SMPTE ST 2110

Linux distribution package

Rivermax SDK

The workload uses Rivermax

Rivermax Getting Started Guide

Example: Ubuntu 24.04

Install the required cloud-init package:

sudo apt-get update
sudo apt-get install -y cloud-init

If the cluster uses KubeVirt guest-agent features, install and enable qemu-guest-agent:

sudo apt-get install -y qemu-guest-agent
sudo systemctl enable --now qemu-guest-agent

Install the DOCA 3.0.0 repository package and DOCA-OFED that the validated Ubuntu image uses:

wget -P /tmp \
  https://content.mellanox.com/DOCA/DOCA_v3.0.0/host/doca-host_3.0.0-058000-25.04-ubuntu2404_amd64.deb
sudo dpkg -i \
  /tmp/doca-host_3.0.0-058000-25.04-ubuntu2404_amd64.deb
sudo apt-get update
sudo apt-get install -y doca-ofed
sudo /etc/init.d/openibd restart

Install the supported NVIDIA GPU driver and enable nvidia-peermem when the workload uses GPUDirect RDMA:

sudo apt-get install -y nvidia-driver-580-open
sudo modprobe nvidia
sudo modprobe nvidia-peermem
echo nvidia-peermem | sudo tee \
  /etc/modules-load.d/nvidia-peermem.conf

If the workload uses CUDA, install its required CUDA Toolkit release. The validated image uses CUDA Toolkit 12.8:

wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2404/x86_64/cuda-ubuntu2404.pin
sudo mv cuda-ubuntu2404.pin \
  /etc/apt/preferences.d/cuda-repository-pin-600
wget https://developer.download.nvidia.com/compute/cuda/12.8.0/local_installers/cuda-repo-ubuntu2404-12-8-local_12.8.0-570.86.10-1_amd64.deb
sudo dpkg -i \
  cuda-repo-ubuntu2404-12-8-local_12.8.0-570.86.10-1_amd64.deb
sudo cp /var/cuda-repo-ubuntu2404-12-8-local/cuda-*-keyring.gpg \
  /usr/share/keyrings/
sudo apt-get update
sudo apt-get install -y cuda-toolkit-12-8

Reboot the VM to load the installed kernel drivers:

sudo reboot

After the VM restarts, verify the installed drivers:

nvidia-smi
ofed_info -s

Install and Validate Rivermax#

For SDK access and compatibility requirements, refer to the Rivermax Getting Started Guide. This Ubuntu 24.04 example was validated with Rivermax 1.80.24, DOCA-OFED 25.04, nvidia-driver-580-open, and CUDA Toolkit 12.8. Use Rivermax 1.80.24 with this example. Newer releases might require a newer DOCA-OFED.

Example: Install and Build on Ubuntu 24.04

Download Rivermax 1.80.24 for Ubuntu 24.04, then extract the archive and install the SDK package:

RIVERMAX_VERSION="1.80.24"
mkdir -p /tmp/rivermax
tar -xzf "rivermax_ubuntu2404_${RIVERMAX_VERSION}.tar.gz" \
  -C /tmp/rivermax
sudo dpkg -i \
  "/tmp/rivermax/${RIVERMAX_VERSION}/Ubuntu.24.04/deb-dist/x86_64/rivermax_${RIVERMAX_VERSION}_amd64.deb"

Install the dependencies that the Rivermax sample applications require:

sudo apt-get install -y \
  avahi-daemon build-essential cmake freeglut3-dev jq \
  libcap-dev libegl1-mesa-dev libgl1-mesa-dev libglu1-mesa-dev \
  libglx-dev libnl-3-dev libnl-genl-3-dev linuxptp \
  python3-pip python3-venv

Build the CUDA-enabled media_sender from the SDK and copy it to a persistent location:

RIVERMAX_VERSION="1.80.24"
cd "/tmp/rivermax/${RIVERMAX_VERSION}/apps"
export PATH="/usr/local/cuda-12.8/bin:${PATH}"
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DRMAX_CUDA=ON
cmake --build build -j"$(nproc)"
sudo cp build/media_sender/media_sender /usr/local/bin/media_sender

Obtain a license by following the NVIDIA Rivermax license procedure. Use the license only for validation and do not include it in the release image. Before selecting the PTP clock, synchronize the PTP hardware clock (PHC) associated with the media interface according to the deployment’s PTP configuration. Do not assume that the PHC is /dev/ptp0. Provide the license and a Session Description Protocol (SDP) file, then run media_sender with the media-interface address. This example uses the PTP clock and assigns separate CPU cores to Rivermax and the application:

sudo env RIVERMAX_LICENSE_PATH=<license-file> \
  /usr/local/bin/media_sender \
  --sdp-file <sender.sdp> --src-ip <media-interface-ip> \
  --internal-core 0 --application-core 1 --wait -v 4

Add --gpu-id 0 to use GPUDirect. Run media_sender --help to inspect the available options.

Verify that the sender starts and selects the expected interface, destination, frame format, PTP clock, and GPU. Remove the temporary SDP and license after validation.

Automate Media Network Initialization#

A deployed VM receives its SR-IOV interface and IP address when KubeVirt starts the guest. Keep this per-VM configuration outside the release image. The Linux workflow uses:

  • A Downward API disk containing the Multus network-status annotation

  • A ConfigMap disk containing the network initialization script and systemd unit

  • cloud-init to install and enable the network initialization artifacts on first boot

At boot, the network initialization service does the following:

  1. Reads network-status.

  2. Matches the media attachment to a Linux interface by MAC address.

  3. Configures the IP address of that interface.

  4. Writes the selected address to /run/h4m/media-ip.

Configure the Media Interface#

Create configure-media-network.sh with the following core logic. This example requires one non-default Multus attachment. For multiple media attachments, select the required entry by its name field instead of taking the first entry. Set <media-prefix-length> to the prefix length defined by the media NetworkAttachmentDefinition. The script uses this value when network-status provides a bare IP address.

#!/bin/bash
set -euo pipefail

mount_dir=/mnt/network-status
status_file=${mount_dir}/network-status
configured_prefix="<media-prefix-length>"
disk=

for candidate in /dev/disk/by-id/virtio-NETINFODISK*; do
    if [[ -e "${candidate}" ]]; then
        disk=$(readlink -f "${candidate}")
        break
    fi
done

[[ -n "${disk}" ]] || { echo "network-status disk not found"; exit 1; }
mkdir -p "${mount_dir}"
mountpoint -q "${mount_dir}" || mount -o ro "${disk}" "${mount_dir}"
trap 'umount "${mount_dir}" 2>/dev/null || true' EXIT

entry=$(jq -c \
  '[.[] | select(.default != true and (.ips | length) > 0)][0]' \
  "${status_file}")
[[ "${entry}" != "null" ]] || { echo "media network not found"; exit 1; }

target_mac=$(jq -r '.mac' <<<"${entry}" | tr -d ':.-' | tr '[:upper:]' '[:lower:]')
ip_value=$(jq -r '.ips[0]' <<<"${entry}")
if [[ "${ip_value}" == */* ]]; then
    address=${ip_value%/*}
    prefix=${ip_value##*/}
else
    address=${ip_value}
    prefix=${configured_prefix}
fi
interface=

for path in /sys/class/net/*; do
    mac=$(tr -d ':.-' < "${path}/address" | tr '[:upper:]' '[:lower:]')
    if [[ "${mac}" == "${target_mac}" ]]; then
        interface=$(basename "${path}")
        break
    fi
done

[[ -n "${interface}" ]] || { echo "media interface not found"; exit 1; }

ip -4 address flush dev "${interface}" scope global
ip address add "${address}/${prefix}" dev "${interface}"
ip link set "${interface}" up
install -d -m 0755 /run/h4m
printf '%s\n' "${address}" > /run/h4m/media-ip
printf '%s\n' "${interface}" > /run/h4m/media-interface

Create configure-media-network.service:

[Unit]
Description=Configure the SR-IOV media interface
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
ExecStart=/opt/h4m/scripts/configure-media-network.sh
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target

Create the VM namespace if it does not exist, then store both files in a ConfigMap:

kubectl create namespace <vm-namespace>
kubectl create configmap linux-vm-startup -n <vm-namespace> \
  --from-file=configure-media-network.sh \
  --from-file=configure-media-network.service

Prepare the Image for Export#

  1. Reboot the source VM and repeat the driver, Rivermax, and media_sender checks.

  2. Remove temporary files, package caches, command history, credentials, host-specific network configuration, SDP test files, and license files.

  3. Clear the cloud-init instance state so it runs when you clone the image:

    sudo cloud-init clean --logs --machine-id
    
  4. Shut down the source VM cleanly.

Create the Release Image#

Run the following commands on the build host after you shut down the source VM. If the hypervisor uses snapshots or differencing disks, first export or consolidate them into a single disk containing the final VM state.

  1. Convert the source disk to a compressed, standalone QCOW2 image:

    qemu-img convert \
      -p -c -f <source-format> -O qcow2 \
      <source-disk> \
      <linux-media-image-version.qcow2>
    

    Set <source-format> to the actual disk format, such as qcow2, vdi, or vmdk.

    For the Virtual Machine Manager example:

    sudo qemu-img convert -p -c -f qcow2 -O qcow2 \
      /var/lib/libvirt/images/ubuntu2404.qcow2 \
      "$HOME/vm-build/ubuntu2404-release.qcow2"
    sudo chown "$(id -u):$(id -g)" \
      "$HOME/vm-build/ubuntu2404-release.qcow2"
    
  2. Validate the release image:

    qemu-img check <linux-media-image-version.qcow2>
    qemu-img info --backing-chain <linux-media-image-version.qcow2>
    

    Verify that the format is qcow2 and that the output lists no backing file.

Upload and Deploy the Release Image#

Follow Upload the QCOW2 Image to upload the release image and create its DataSource. When you reach Create a VM from the DataSource, add the following configuration to virtual-machine.yaml before applying it.

Attach the Startup Data to the VM#

The example source image uses UEFI with Secure Boot disabled. The deployed VM must use the same firmware profile. Extend the manifest with the firmware, Linux network initialization disks, SR-IOV interface, and optional GPU:

spec:
  template:
    spec:
      domain:
        firmware:
          bootloader:
            efi:
              secureBoot: false
        devices:
          disks:
            - name: cloudinit
              disk:
                bus: virtio
            - name: network-status
              serial: NETINFODISK
              disk: {}
            - name: startup-scripts
              serial: LNXSTARTUP
              disk: {}
          interfaces:
            - name: media-a
              sriov: {}
          gpus:
            - name: gpu
              deviceName: <permitted-gpu-resource>
      networks:
        - name: media-a
          multus:
            networkName: <media-network-attachment-definition>
      volumes:
        - name: cloudinit
          cloudInitNoCloud:
            userData: |
              #cloud-config
              runcmd:
                - |
                  set -eu
                  mkdir -p /mnt/linux-vm-startup /opt/h4m/scripts
                  disk=$(readlink -f /dev/disk/by-id/virtio-LNXSTARTUP)
                  mount -o ro "$disk" /mnt/linux-vm-startup
                  install -m 0755 /mnt/linux-vm-startup/configure-media-network.sh /opt/h4m/scripts/
                  install -m 0644 /mnt/linux-vm-startup/configure-media-network.service /etc/systemd/system/
                  umount /mnt/linux-vm-startup
                  systemctl daemon-reload
                  systemctl enable --now configure-media-network.service
        - name: network-status
          downwardAPI:
            fields:
              - path: network-status
                fieldRef:
                  fieldPath: metadata.annotations['k8s.v1.cni.cncf.io/network-status']
        - name: startup-scripts
          configMap:
            name: linux-vm-startup

After the deployed VM starts, run the following commands inside the guest:

cloud-init status --long
systemctl status configure-media-network.service
cat /run/h4m/media-ip
cat /run/h4m/media-interface
ip address show