From 57d267db7878180d1ecd1936df5284550d0031c3 Mon Sep 17 00:00:00 2001 From: Bruce Ashfield Date: Sun, 15 Feb 2026 04:35:55 +0000 Subject: vxn: add Xen DomU container runtime with OCI image support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit vxn runs OCI containers as Xen DomU guests — the VM IS the container. No Docker/containerd runs inside the guest; the init script directly mounts the container rootfs and execs the entrypoint via chroot. Host-side (Dom0): - vxn.sh: Docker-like CLI wrapper (sets HYPERVISOR=xen) - vrunner-backend-xen.sh: Xen xl backend for vrunner - hv_prepare_container(): pulls OCI images via skopeo, resolves entrypoint from OCI config using jq on host - xl create for VM lifecycle (PVH on aarch64, PV on x86_64) - Bridge networking with iptables DNAT for port forwards - Console capture via xl console for ephemeral mode Guest-side (DomU): - vxn-init.sh: mounts container rootfs from input disk, extracts OCI layers, execs entrypoint via chroot - Supports containers with or without /bin/sh - grep/sed fallback for OCI config parsing (no jq needed) - Daemon mode with command loop on hvc1 - vcontainer-init-common.sh: hypervisor detection, head -n fix - vcontainer-preinit.sh: init selection via vcontainer.init= Build system: - vxn-initramfs-create.inc: assembles boot blobs from vruntime multiconfig, injects vxn-init.sh into rootfs squashfs - vxn_1.0.bb: Dom0 package with scripts + blobs - nostamp on install/package chain (blobs from DEPLOY_DIR are untracked by sstate) - vxn.cfg: Xen PV kernel config fragment Tested: vxn -it --no-daemon run --rm hello-world Signed-off-by: Bruce Ashfield --- recipes-core/vxn/vxn-initramfs-create.inc | 223 +++++++++++++++++++++++++++ recipes-core/vxn/vxn-initramfs-create_1.0.bb | 43 ++++++ recipes-core/vxn/vxn_1.0.bb | 167 ++++++++++++++++++++ 3 files changed, 433 insertions(+) create mode 100644 recipes-core/vxn/vxn-initramfs-create.inc create mode 100644 recipes-core/vxn/vxn-initramfs-create_1.0.bb create mode 100644 recipes-core/vxn/vxn_1.0.bb (limited to 'recipes-core') diff --git a/recipes-core/vxn/vxn-initramfs-create.inc b/recipes-core/vxn/vxn-initramfs-create.inc new file mode 100644 index 00000000..bde8bba9 --- /dev/null +++ b/recipes-core/vxn/vxn-initramfs-create.inc @@ -0,0 +1,223 @@ +# SPDX-FileCopyrightText: Copyright (C) 2025 Bruce Ashfield +# +# SPDX-License-Identifier: MIT +# +# vxn-initramfs-create.inc +# =========================================================================== +# Shared code for building Xen DomU boot blobs for vxn +# =========================================================================== +# +# This .inc file packages boot blobs for vxn (vcontainer on Xen). +# It reuses the same initramfs and rootfs images built by vruntime +# multiconfig (same images as vdkr/vpdmn), since the init scripts +# detect the hypervisor at boot time. +# +# The kernel from vruntime already includes Xen PV support via vxn.cfg +# fragment (added to DISTRO_FEATURES in vruntime.conf). +# +# Required variables from including recipe: +# VXN_RUNTIME - runtime to source blobs from ("vdkr" or "vpdmn") +# +# Boot flow on Xen Dom0: +# xl create +# -> Xen boots kernel + tiny initramfs in DomU +# -> preinit mounts rootfs.img from /dev/xvda +# -> switch_root into rootfs.img +# -> init detects Xen, uses /dev/xvd* and trans=xen +# +# =========================================================================== + +HOMEPAGE = "https://git.yoctoproject.org/meta-virtualization/" +LICENSE = "MIT" +LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4f302" + +inherit deploy + +EXCLUDE_FROM_WORLD = "1" +DEPENDS = "squashfs-tools-native" + +# Default to vdkr (Docker) as the source runtime +VXN_RUNTIME ?= "vdkr" + +# Always rebuild - init script injection must not be sstate-cached +# because the rootfs.img content comes from the deploy dir (untracked) +SSTATE_SKIP_CREATION = "1" +do_compile[nostamp] = "1" +do_deploy[nostamp] = "1" + +python () { + + mc = d.getVar('VXN_MULTICONFIG') + runtime = d.getVar('VXN_RUNTIME') + bbmulticonfig = (d.getVar('BBMULTICONFIG') or "").split() + if mc in bbmulticonfig: + # All blobs come from the vruntime multiconfig - kernel, initramfs, rootfs + mcdeps = ' '.join([ + 'mc::%s:%s-tiny-initramfs-image:do_image_complete' % (mc, runtime), + 'mc::%s:%s-rootfs-image:do_image_complete' % (mc, runtime), + 'mc::%s:virtual/kernel:do_deploy' % mc, + ]) + d.setVarFlag('do_compile', 'mcdepends', mcdeps) +} + +INHIBIT_DEFAULT_DEPS = "1" + +# Init scripts to inject into the rootfs squashfs +FILESEXTRAPATHS:prepend := "${THISDIR}/../../recipes-containers/vcontainer/files:" +SRC_URI = "\ + file://vxn-init.sh \ + file://vcontainer-init-common.sh \ +" + +S = "${UNPACKDIR}" +B = "${WORKDIR}/build" + +def vxn_get_kernel_image_name(d): + arch = d.getVar('TARGET_ARCH') + if arch == 'aarch64': + return 'Image' + elif arch in ['x86_64', 'i686', 'i586']: + return 'bzImage' + elif arch == 'arm': + return 'zImage' + return 'Image' + +def vxn_get_multiconfig_name(d): + arch = d.getVar('TARGET_ARCH') + if arch == 'aarch64': + return 'vruntime-aarch64' + elif arch in ['x86_64', 'i686', 'i586']: + return 'vruntime-x86-64' + return 'vruntime-aarch64' + +def vxn_get_blob_arch(d): + arch = d.getVar('TARGET_ARCH') + if arch == 'aarch64': + return 'aarch64' + elif arch in ['x86_64', 'i686', 'i586']: + return 'x86_64' + return 'aarch64' + +KERNEL_IMAGETYPE_INITRAMFS = "${@vxn_get_kernel_image_name(d)}" +VXN_MULTICONFIG = "${@vxn_get_multiconfig_name(d)}" +BLOB_ARCH = "${@vxn_get_blob_arch(d)}" + +VXN_MC_DEPLOY = "${TOPDIR}/tmp-${VXN_MULTICONFIG}/deploy/images/${MACHINE}" + +do_compile() { + mkdir -p ${B} + + MC_TMPDIR="${TOPDIR}/tmp-${VXN_MULTICONFIG}" + MC_DEPLOY="${MC_TMPDIR}/deploy/images/${MACHINE}" + + # ========================================================================= + # PART 1: COPY TINY INITRAMFS (same as vdkr/vpdmn) + # ========================================================================= + bbnote "Copying tiny initramfs from image build..." + + INITRAMFS_SRC="${MC_DEPLOY}/${VXN_RUNTIME}-tiny-initramfs-image-${MACHINE}.cpio.gz" + + if [ ! -f "${INITRAMFS_SRC}" ]; then + bbfatal "Initramfs not found at ${INITRAMFS_SRC}. Build it first with: bitbake mc:${VXN_MULTICONFIG}:${VXN_RUNTIME}-tiny-initramfs-image" + fi + + cp "${INITRAMFS_SRC}" ${B}/initramfs.cpio.gz + INITRAMFS_SIZE=$(stat -c%s ${B}/initramfs.cpio.gz) + bbnote "Initramfs copied: ${INITRAMFS_SIZE} bytes ($(expr ${INITRAMFS_SIZE} / 1024)KB)" + + # ========================================================================= + # PART 2: COPY ROOTFS (same squashfs, works under both QEMU and Xen) + # ========================================================================= + bbnote "Copying rootfs from image build..." + + ROOTFS_SRC="${MC_DEPLOY}/${VXN_RUNTIME}-rootfs-image-${MACHINE}.rootfs.squashfs" + + if [ ! -f "${ROOTFS_SRC}" ]; then + bbfatal "Rootfs image not found at ${ROOTFS_SRC}. Build it first with: bitbake mc:${VXN_MULTICONFIG}:${VXN_RUNTIME}-rootfs-image" + fi + + cp "${ROOTFS_SRC}" ${B}/rootfs.img + ROOTFS_SIZE=$(stat -c%s ${B}/rootfs.img) + bbnote "Rootfs image copied: ${ROOTFS_SIZE} bytes ($(expr ${ROOTFS_SIZE} / 1024 / 1024)MB)" + + # Inject vxn init scripts into the rootfs squashfs + bbnote "Injecting vxn init scripts into rootfs..." + UNSQUASH_DIR="${B}/rootfs-unsquash" + rm -rf "${UNSQUASH_DIR}" + unsquashfs -d "${UNSQUASH_DIR}" ${B}/rootfs.img + install -m 0755 ${S}/vxn-init.sh ${UNSQUASH_DIR}/vxn-init.sh + install -m 0755 ${S}/vcontainer-init-common.sh ${UNSQUASH_DIR}/vcontainer-init-common.sh + rm -f ${B}/rootfs.img + mksquashfs "${UNSQUASH_DIR}" ${B}/rootfs.img -noappend -comp xz + rm -rf "${UNSQUASH_DIR}" + ROOTFS_SIZE=$(stat -c%s ${B}/rootfs.img) + bbnote "Rootfs with vxn init scripts: ${ROOTFS_SIZE} bytes ($(expr ${ROOTFS_SIZE} / 1024 / 1024)MB)" + + # ========================================================================= + # PART 3: COPY KERNEL (Xen PV-capable via vxn.cfg fragment) + # ========================================================================= + bbnote "Copying kernel image..." + KERNEL_FILE="${DEPLOY_DIR_IMAGE}/${KERNEL_IMAGETYPE_INITRAMFS}" + if [ -f "${KERNEL_FILE}" ]; then + cp "${KERNEL_FILE}" ${B}/kernel + KERNEL_SIZE=$(stat -c%s ${B}/kernel) + bbnote "Kernel copied: ${KERNEL_SIZE} bytes ($(expr ${KERNEL_SIZE} / 1024 / 1024)MB)" + else + bbwarn "Kernel not found at ${KERNEL_FILE}" + fi +} + +# This is a deploy-only recipe - no packages produced. +# PACKAGES="" prevents the rootfs task from looking for package manifests. +PACKAGES = "" +do_install[noexec] = "1" +do_package[noexec] = "1" +do_packagedata[noexec] = "1" +do_package_write_rpm[noexec] = "1" +do_package_write_ipk[noexec] = "1" +do_package_write_deb[noexec] = "1" +do_populate_sysroot[noexec] = "1" + +do_deploy() { + install -d ${DEPLOYDIR}/vxn/${BLOB_ARCH} + + if [ -f ${B}/initramfs.cpio.gz ]; then + install -m 0644 ${B}/initramfs.cpio.gz ${DEPLOYDIR}/vxn/${BLOB_ARCH}/ + bbnote "Deployed initramfs.cpio.gz to vxn/${BLOB_ARCH}/" + fi + + if [ -f ${B}/rootfs.img ]; then + install -m 0644 ${B}/rootfs.img ${DEPLOYDIR}/vxn/${BLOB_ARCH}/ + bbnote "Deployed rootfs.img to vxn/${BLOB_ARCH}/" + fi + + if [ -f ${B}/kernel ]; then + install -m 0644 ${B}/kernel ${DEPLOYDIR}/vxn/${BLOB_ARCH}/${KERNEL_IMAGETYPE_INITRAMFS} + bbnote "Deployed kernel as vxn/${BLOB_ARCH}/${KERNEL_IMAGETYPE_INITRAMFS}" + fi + + cat > ${DEPLOYDIR}/vxn/${BLOB_ARCH}/README << EOF +vxn Boot Blobs (Xen DomU) +========================== + +Built for: ${TARGET_ARCH} +Machine: ${MACHINE} +Multiconfig: ${VXN_MULTICONFIG} +Source runtime: ${VXN_RUNTIME} +Date: $(date) + +Files: + ${KERNEL_IMAGETYPE_INITRAMFS} - Kernel image (Xen PV-capable) + initramfs.cpio.gz - Tiny initramfs (busybox + preinit) + rootfs.img - Root filesystem with container tools + +Boot flow: + xl create + -> Xen boots kernel + initramfs in DomU + -> preinit detects Xen, mounts rootfs.img from /dev/xvda + -> switch_root into rootfs.img + -> init script runs container commands +EOF +} + +addtask deploy after do_compile before do_build diff --git a/recipes-core/vxn/vxn-initramfs-create_1.0.bb b/recipes-core/vxn/vxn-initramfs-create_1.0.bb new file mode 100644 index 00000000..edbef12f --- /dev/null +++ b/recipes-core/vxn/vxn-initramfs-create_1.0.bb @@ -0,0 +1,43 @@ +# SPDX-FileCopyrightText: Copyright (C) 2025 Bruce Ashfield +# +# SPDX-License-Identifier: MIT +# +# vxn-initramfs-create_1.0.bb +# =========================================================================== +# Builds Xen DomU boot blobs for vxn +# =========================================================================== +# +# This recipe packages boot blobs for vxn (vcontainer on Xen): +# - A tiny initramfs (reused from vdkr/vpdmn build) +# - The rootfs.img squashfs (same as vdkr, with HV detection in init) +# - The kernel (Xen PV-capable via vxn.cfg fragment in vruntime) +# +# Boot flow on Xen Dom0: +# xl create domain.cfg +# -> Xen boots kernel + tiny initramfs in DomU +# -> preinit detects Xen block prefix, mounts rootfs.img from /dev/xvda +# -> switch_root into rootfs.img +# -> vdkr-init.sh detects Xen via /proc/xen, uses xvd* devices +# +# =========================================================================== +# BUILD INSTRUCTIONS +# =========================================================================== +# +# For aarch64: +# MACHINE=qemuarm64 bitbake vxn-initramfs-create +# +# For x86_64: +# MACHINE=qemux86-64 bitbake vxn-initramfs-create +# +# Blobs are deployed to: tmp/deploy/images/${MACHINE}/vxn/ +# +# =========================================================================== + +SUMMARY = "Build Xen DomU boot blobs for vxn" +DESCRIPTION = "Packages kernel, initramfs and rootfs for running \ + vcontainer workloads as Xen DomU guests." + +# Source blobs from vdkr (Docker) build - same rootfs works under Xen +VXN_RUNTIME = "vdkr" + +require vxn-initramfs-create.inc diff --git a/recipes-core/vxn/vxn_1.0.bb b/recipes-core/vxn/vxn_1.0.bb new file mode 100644 index 00000000..2a36274a --- /dev/null +++ b/recipes-core/vxn/vxn_1.0.bb @@ -0,0 +1,167 @@ +# SPDX-FileCopyrightText: Copyright (C) 2025 Bruce Ashfield +# +# SPDX-License-Identifier: MIT +# +# vxn_1.0.bb +# =========================================================================== +# Target integration package for vxn (vcontainer on Xen) +# =========================================================================== +# +# This recipe installs vxn onto a Xen Dom0 target. It provides: +# - vxn CLI wrapper (docker-like interface for Xen DomU containers) +# - vrunner.sh (hypervisor-agnostic VM runner) +# - vrunner-backend-xen.sh (Xen xl backend) +# - vcontainer-common.sh (shared CLI code) +# - Kernel, initramfs, and rootfs blobs for booting DomU guests +# +# The blobs are sourced from the vxn-initramfs-create recipe which +# reuses the same rootfs images built by vdkr/vpdmn (the init scripts +# detect the hypervisor at boot time). +# +# =========================================================================== +# BUILD INSTRUCTIONS +# =========================================================================== +# +# For aarch64 Dom0: +# MACHINE=qemuarm64 bitbake vxn +# +# For x86_64 Dom0: +# MACHINE=qemux86-64 bitbake vxn +# +# Add to a Dom0 image: +# IMAGE_INSTALL:append = " vxn" +# +# Usage on Dom0: +# vxn run hello-world # Run OCI container as Xen DomU +# vxn vmemres start # Start persistent DomU (daemon mode) +# vxn vexpose # Expose Docker API on Dom0 +# +# =========================================================================== + +SUMMARY = "Docker CLI for Xen-based container execution" +DESCRIPTION = "vxn provides a familiar docker-like CLI that executes commands \ + inside a Xen DomU guest with Docker. It uses the vcontainer \ + infrastructure with a Xen hypervisor backend." +HOMEPAGE = "https://git.yoctoproject.org/meta-virtualization/" +LICENSE = "MIT" +LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4f302" + +inherit features_check +REQUIRED_DISTRO_FEATURES = "xen" + +SRC_URI = "\ + file://vxn.sh \ + file://vrunner.sh \ + file://vrunner-backend-xen.sh \ + file://vrunner-backend-qemu.sh \ + file://vcontainer-common.sh \ +" + +FILESEXTRAPATHS:prepend := "${THISDIR}/../../recipes-containers/vcontainer/files:" + +S = "${UNPACKDIR}" + +# Runtime dependencies on Dom0 +RDEPENDS:${PN} = "\ + xen-tools-xl \ + bash \ + jq \ + socat \ + coreutils \ + util-linux \ + e2fsprogs \ + skopeo \ +" + +# Blobs are sourced from vxn-initramfs-create deploy output. +# Build blobs first: bitbake vxn-initramfs-create +# No task dependency here - vxn-initramfs-create is deploy-only (no packages). +# Adding any dependency from a packaged recipe to a deploy-only recipe +# breaks do_rootfs (sstate manifest not found for package_write_rpm). + +# Blobs come from DEPLOY_DIR which is untracked by sstate hash. +# nostamp on do_install alone is insufficient — do_package and +# do_package_write_rpm have unchanged sstate hashes so they restore +# the OLD RPM from cache, discarding the fresh do_install output. +# Force the entire install→package→RPM chain to always re-run. +do_install[nostamp] = "1" +do_package[nostamp] = "1" +do_packagedata[nostamp] = "1" +do_package_write_rpm[nostamp] = "1" +do_package_write_ipk[nostamp] = "1" +do_package_write_deb[nostamp] = "1" + +def vxn_get_blob_arch(d): + arch = d.getVar('TARGET_ARCH') + if arch == 'aarch64': + return 'aarch64' + elif arch in ['x86_64', 'i686', 'i586']: + return 'x86_64' + return 'aarch64' + +def vxn_get_kernel_image_name(d): + arch = d.getVar('TARGET_ARCH') + if arch == 'aarch64': + return 'Image' + elif arch in ['x86_64', 'i686', 'i586']: + return 'bzImage' + elif arch == 'arm': + return 'zImage' + return 'Image' + +BLOB_ARCH = "${@vxn_get_blob_arch(d)}" +KERNEL_IMAGETYPE_VXN = "${@vxn_get_kernel_image_name(d)}" + +VXN_DEPLOY = "${DEPLOY_DIR_IMAGE}" + +do_install() { + # Install CLI wrapper + install -d ${D}${bindir} + install -m 0755 ${S}/vxn.sh ${D}${bindir}/vxn + + # Install shared scripts into libdir + install -d ${D}${libdir}/vxn + install -m 0755 ${S}/vrunner.sh ${D}${libdir}/vxn/ + install -m 0755 ${S}/vrunner-backend-xen.sh ${D}${libdir}/vxn/ + install -m 0755 ${S}/vrunner-backend-qemu.sh ${D}${libdir}/vxn/ + install -m 0644 ${S}/vcontainer-common.sh ${D}${libdir}/vxn/ + + # Install blobs from vxn-initramfs-create deployment + # Layout must match what vrunner backends expect: $BLOB_DIR//{Image,initramfs.cpio.gz,rootfs.img} + install -d ${D}${datadir}/vxn/${BLOB_ARCH} + + VXN_BLOB_SRC="${VXN_DEPLOY}/vxn/${BLOB_ARCH}" + if [ -d "${VXN_BLOB_SRC}" ]; then + if [ -f "${VXN_BLOB_SRC}/${KERNEL_IMAGETYPE_VXN}" ]; then + install -m 0644 "${VXN_BLOB_SRC}/${KERNEL_IMAGETYPE_VXN}" ${D}${datadir}/vxn/${BLOB_ARCH}/ + bbnote "Installed kernel ${KERNEL_IMAGETYPE_VXN}" + else + bbwarn "Kernel not found at ${VXN_BLOB_SRC}/${KERNEL_IMAGETYPE_VXN}" + fi + + if [ -f "${VXN_BLOB_SRC}/initramfs.cpio.gz" ]; then + install -m 0644 "${VXN_BLOB_SRC}/initramfs.cpio.gz" ${D}${datadir}/vxn/${BLOB_ARCH}/ + bbnote "Installed initramfs" + else + bbwarn "Initramfs not found at ${VXN_BLOB_SRC}/initramfs.cpio.gz" + fi + + if [ -f "${VXN_BLOB_SRC}/rootfs.img" ]; then + install -m 0644 "${VXN_BLOB_SRC}/rootfs.img" ${D}${datadir}/vxn/${BLOB_ARCH}/ + bbnote "Installed rootfs.img" + else + bbwarn "Rootfs not found at ${VXN_BLOB_SRC}/rootfs.img" + fi + else + bbwarn "VXN blob directory not found at ${VXN_BLOB_SRC}. Build with: bitbake vxn-initramfs-create" + fi +} + +FILES:${PN} = "\ + ${bindir}/vxn \ + ${libdir}/vxn/ \ + ${datadir}/vxn/ \ +" + +# Blobs are large binary files +INSANE_SKIP:${PN} += "already-stripped" -- cgit v1.2.3-54-g00ecf