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:
On a build host, install a hypervisor and
qemu-img.Verify the Linux installation ISO and create a VM with firmware, machine type, and disk bus supported by the target KubeVirt environment.
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.
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#
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
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#
Create a build directory:
mkdir -p "$HOME/vm-build" cd "$HOME/vm-build"
Download the Ubuntu Server 24.04 ISO and its
SHA256SUMSfile from the Ubuntu release server.Verify the downloaded ISO:
sha256sum -c SHA256SUMS --ignore-missing
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#
Start Virtual Machine Manager and select QEMU/KVM - System.
Select File > New Virtual Machine > Local install media.
Select Use ISO image > Browse, select the
defaultstorage pool andubuntu-server-24.04.iso, then select Choose Volume. Select Ubuntu 24.04 manually if Virtual Machine Manager does not detect it.Configure
4vCPUs and8192 MiBof memory.Select Create a disk image for the virtual machine, set its capacity to
80 GiB, and continue.Name the VM
ubuntu2404and select Customize configuration before install.In Overview, select the
Q35chipset and UEFI x86_64: /usr/share/OVMF/OVMF_CODE_4M.fd (the UEFI firmware without Secure Boot). Set the system disk bus toVirtIOand the network interface device model tovirtio.In Boot Options, place the installation CD-ROM before the system disk, then select Begin Installation.
In the Ubuntu installer, install Ubuntu Server on the entire virtual disk, create an administrator account, and complete the installation.
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
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:
Shut down
ubuntu2404and open its hardware details in Virtual Machine Manager.Select Add Hardware > PCI Host Device and add the NVIDIA GPU. Add every required function of a multifunction device.
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.
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 |
|
DOCA-OFED or MLNX_OFED |
The workload uses a ConnectX NIC or BlueField data processing unit (DPU) |
|
CUDA Toolkit |
The application uses CUDA |
|
|
The workload requires Precision Time Protocol (PTP) synchronization, including SMPTE ST 2110 |
Linux distribution package |
Rivermax SDK |
The workload uses Rivermax |
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-statusannotationA ConfigMap disk containing the network initialization script and
systemdunitcloud-initto install and enable the network initialization artifacts on first boot
At boot, the network initialization service does the following:
Reads
network-status.Matches the media attachment to a Linux interface by MAC address.
Configures the IP address of that interface.
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#
Reboot the source VM and repeat the driver, Rivermax, and
media_senderchecks.Remove temporary files, package caches, command history, credentials, host-specific network configuration, SDP test files, and license files.
Clear the
cloud-initinstance state so it runs when you clone the image:sudo cloud-init clean --logs --machine-id
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.
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 asqcow2,vdi, orvmdk.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"
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
qcow2and 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