diff --git a/centos-build-opennebula-image.sh b/centos-build-opennebula-image.sh
new file mode 100755
index 0000000..6a8fe31
--- /dev/null
+++ b/centos-build-opennebula-image.sh
@@ -0,0 +1,170 @@
+#!/bin/sh
+
+# This script generates CentOS images for OpenNebula.
+
+# Depends on the following packages (as of CentOS 8):
+#   qemu-img util-linux coreutils dnf curl e2fsprogs
+
+# Run locally (without network) with:
+#   qemu-system-x86_64 -enable-kvm -m 1G -drive file=$IMAGE,format=qcow2
+
+set -e
+set -x
+
+# XXX: Handle command-line arguments?
+RELEASE=8
+ARCH=x86_64
+IMAGE_PATH=centos-$RELEASE-$(date --iso-8601).img
+IMAGE_SIZE=10G
+LOOPBACK_DEVICE=/dev/loop0
+
+# TODO: find the package definition and built ourself, publish in some RPM repository.
+ONE_CONTEXT_RPM_URL="https://github.com/OpenNebula/addon-context-linux/releases/download/v5.10.0/one-context-5.10.0-1.el8.noarch.rpm"
+ONE_CONTEXT_RPM_PATH=/root/one-context.rpm
+
+cleanup() {
+	# The order here is important.
+	umount /mnt/dev/pts 2>/dev/null || true
+	umount /mnt/dev/shm 2>/dev/null || true
+	umount /mnt/dev 2>/dev/null || true
+	umount /mnt/proc 2>/dev/null || true
+	umount /mnt/run 2>/dev/null || true
+	umount /mnt/sys 2>/dev/null || true
+	umount /mnt/boot 2>/dev/null || true
+	umount /mnt 2>/dev/null || true
+	losetup -d "$LOOPBACK_DEVICE"
+}
+
+run_root() {
+	chroot /mnt /usr/bin/env \
+		PATH=/sbin:/usr/sbin:/bin:/usr/bin \
+		sh -c "$*"
+}
+
+if [ "$(whoami)" != 'root' ]; then
+	echo "This script must be run as root." >&2
+	exit 1
+fi
+
+if [ ! -f '/etc/centos-release' ]; then
+	echo "WARNING: this script has been designed to run on a CentOS system." >&2
+	echo "WARNING: Not running CentOS. Giving you 5 seconds to abort." >&2
+	sleep 5
+fi
+
+# Create base RAW image (no LOOPBACK support in RHEL/CentOS).
+qemu-img create -f raw "$IMAGE_PATH" "$IMAGE_SIZE"
+losetup "$LOOPBACK_DEVICE" "$IMAGE_PATH"
+
+# Don't forget to cleanup, even if the script crash.
+trap cleanup EXIT
+
+# Create partition table, format partitions.
+{
+sfdisk --no-reread "$LOOPBACK_DEVICE" <<EOF
+1M,500M,L,*
+,,L
+EOF
+} || true
+
+partprobe "$LOOPBACK_DEVICE"
+
+mkfs.ext4 "${LOOPBACK_DEVICE}p1"
+mkfs.ext4 "${LOOPBACK_DEVICE}p2"
+
+# Mount partitions, install base OS.
+mount "${LOOPBACK_DEVICE}p2" /mnt
+mkdir /mnt/boot
+mount "${LOOPBACK_DEVICE}p1" /mnt/boot
+
+dnf -y \
+	--releasever=$RELEASE \
+	--installroot=/mnt \
+	--disablerepo='*' \
+	--enablerepo=BaseOS \
+	--enablerepo=AppStream \
+	--enablerepo=extras \
+	--setopt=install_weak_deps=False install \
+	bash basesystem systemd systemd-udev dnf centos-release
+
+mount --bind /dev /mnt/dev
+mount --bind /dev/pts /mnt/dev/pts
+mount --bind /dev/shm /mnt/dev/shm
+mount --bind /proc /mnt/proc
+mount --bind /run /mnt/run
+mount --bind /sys /mnt/sys
+
+# Guest networking is to be handled by the one-context package.
+# See https://github.com/OpenNebula/addon-context-linux for details.
+# Note: as of writing, one-context does not support NetworkManager or
+# systemd-networkd.
+
+# Required to resolve package mirror in chroot.
+cp /etc/resolv.conf /mnt/etc/resolv.conf
+
+# Initialize /etc/hosts.
+cat > /mnt/etc/hosts << EOF
+127.0.0.1   localhost localhost.localdomain localhost4 localhost4.localdomain4
+::1         localhost localhost.localdomain localhost6 localhost6.localdomain6
+
+EOF
+
+# See https://github.com/OpenNebula/addon-context-linux/issues/121 for details.
+# network-scripts.x86_64 : Legacy scripts for manipulating of network devices
+run_root dnf -y install network-scripts
+
+# Install (magic?) one-context RPM and hope things works as expected.
+curl -L "$ONE_CONTEXT_RPM_URL" > "/mnt$ONE_CONTEXT_RPM_PATH"
+run_root dnf -y install "$ONE_CONTEXT_RPM_PATH"
+run_root rm "$ONE_CONTEXT_RPM_PATH"
+
+# Install resize2fs, which is required to resize the root file-system.
+run_root dnf -y install e2fsprogs
+
+# Initalize base services.
+run_root systemd-machine-id-setup
+run_root ln -sf /usr/share/zoneinfo/UTC /etc/localtime
+
+# Install and configure NTP client.
+run_root dnf install -y chrony
+run_root systemctl enable chronyd.service
+
+# Install kernel and bootloader.
+# Note: linux-firmware is not required our environment and takes almost 200M
+# uncompressed but is a direct dependency of kernel-core...
+run_root dnf -y install kernel grub2
+
+# Add support for virtio block devices at boot time.
+cat > /mnt/etc/dracut.conf.d/virtio-blk.conf <<EOF
+add_drivers="virtio-blk"
+EOF
+kernel_version=$(ls /mnt/boot | grep "vmlinuz.*.$ARCH" | cut -d- -f2-)
+run_root dracut --force --kver $kernel_version
+
+# Configure grub2.
+run_root grub2-install --target=i386-pc "${LOOPBACK_DEVICE}"
+run_root grub2-mkconfig -o /boot/grub2/grub.cfg
+
+# Install en configure SSH daemon.
+run_root dnf -y install openssh-server
+run_root systemctl enable sshd
+
+# Generate fstab file.
+boot_uuid=$(blkid --match-tag UUID --output value "${LOOPBACK_DEVICE}p1")
+root_uuid=$(blkid --match-tag UUID --output value "${LOOPBACK_DEVICE}p2")
+cat >>/mnt/etc/fstab <<EOF
+UUID=$boot_uuid /boot ext4 rw,relatime,data=ordered 0 2
+UUID=$root_uuid /     ext4 rw,relatime,data=ordered 0 1
+EOF
+
+# Reset systemd's environment.
+run_root rm -f /etc/machine-id
+run_root touch /etc/machine-id
+rm -f /var/lib/systemd/random-seed
+
+# Remove temporary files and reclaim freed disk space.
+# Note: build logs could be removed as well.
+run_root dnf clean all
+
+# Make sure everything is written to disk before exiting.
+sync