Cnuas VM Lifecycle, design¶
Status: design, Phase 6.2. Author: Cnuas project. Companion docs: -
Cnuas_CI_Strategy.md(Phase 6.0e, ADO self-hosted agents). -Cnuas_Build_System.md(Bazel / Make for component builds). Audience: developers running Cnuas locally, CI engineers wiring ADO pipelines to consume the golden images, and packagers producing Debian artefacts.
1. Why this exists¶
The Phase 1, 5 scripts/vm-manager.sh was a developer hack: it downloaded
a stock Ubuntu cloud image, dropped a cloud-init seed, and booted QEMU
in the user's terminal. Drivers were not installed; the user had to
build cnuasgpu.ko inside the running VM against linux-headers-$(uname -r)
of whatever kernel the cloud image happened to ship. Userspace
(libcnuasrt, cnuaslink-cli, cnuas-vswitchd) had to be copied in by
hand or via an ad-hoc genisoimage payload.
That works for the original author and nobody else. Phase 6.2 replaces it with a proper VM lifecycle pipeline:
- A golden image (
cnuas-vm-vX.Y.Z.qcow2) with all Cnuas userspace.debs installed plus a pinned6.19.0-cnuas+kernel and its pre-built.komodules (cnuasgpu,cnuas_net,cnuas_ib), cloud-init configured for SSH-from-host. - A Python tooling package (
cnuas-tools) with a Typer CLI that builds the image, builds the.debs (kernel + drivers + userspace), and brings up VMs through libvirt. - A second, parallel boot path for kernel developers who want to
iterate on
cnuasgpu.koagainst the in-treelinux/submodule, that stays a thin script (scripts/boot-vm-kernel-dev.sh) for now.
The golden-image path is Approach A; the kernel-dev path is
Approach C in the design-discussion shorthand. Approach B (DKMS
in the guest) is rejected for v1 because we pin a single kernel
and ship its .kos pre-built; DKMS can be re-introduced later for
multi-kernel support (see §13.2).
2. Goals and non-goals¶
Goals¶
| # | Goal |
|---|---|
| G1 | A user can run cnuas-tools vm up vm-a on a fresh machine and have /dev/cnuasgpu0 plus libcnuasrt.so working in the guest in < 30 seconds of wall time. |
| G2 | The golden image is reproducible: same inputs (Ubuntu base image, the pinned linux/ submodule commit, our .debs, recipe) produce a byte-identical *.qcow2 plus a manifest with sha256s. |
| G3 | Tooling is pure-Python (Poetry-managed). External CLI tools (virsh, qemu-img, virt-customize, genisoimage) are wrapped through a single run() helper so a future libvirt-native engine is a drop-in replacement. |
| G4 | The same code path is used by humans and by CI (ADO pipelines). The pipelines just call the same Typer subcommands. |
| G5 | Versioning: every artefact (.deb, .qcow2) is stamped with a semantic version + git SHA + manifest. |
| G6 | The guest runs one known kernel, 6.19.0-cnuas+ from the in-tree linux/ submodule. Users don't build kernels; users don't run DKMS; users don't see "module verification failed" surprises. See §13. |
Non-goals (now)¶
| # | Non-goal | Phase that covers it |
|---|---|---|
| N1 | A native libvirt-python engine. (Subprocess + virsh is fine for v1.) | Phase 6.3 |
| N2 | Publishing images to GitHub Releases automatically. | Phase 6.4 (CI) |
| N3 | An OS image other than Ubuntu 24.04 Noble. | Future |
| N4 | An architecture other than x86_64. |
Phase 7+ |
| N5 | Live-migration, snapshots, pools, anything operational beyond up/down. | Future |
| N6 | Replacing the Bazel/Make builds for the C/C++ components. We call them; we don't replace them. | Never |
| N7 | Supporting non-Cnuas kernels in the guest (DKMS path). Multi-kernel support can be added in a follow-up release if needed; see §13.2. | Post-USENIX |
| N8 | Upstreaming the Cnuas kernel patch / drivers to mainline. We will, but only after the USENIX Cnuas paper publishes. | Post-USENIX |
3. Architecture overview¶
4. The Python tooling package (cnuas-tools)¶
4.1 Project layout¶
cnuas/
tools/
pyproject.toml # Poetry; details in §4.2
poetry.lock # checked in
README.md
.pre-commit-config.yaml
src/
cnuas_tools/
__init__.py # exposes __version__
cli.py # Typer root: `cnuas-tools <subcmd>`
config.py # global config, search paths, env vars
log.py # rich-based logging
run.py # subprocess.run wrapper with proper errors
version.py # reads version from pyproject
pkg/ # `cnuas-tools pkg ...`
__init__.py
cli.py
binary.py # plain (non-DKMS) .deb assembler
kmod.py # kernel-module .deb helper
python_cli.py # Python wheel + wheelhouse + venv .deb
targets/
_core.py # target registry / ABC
cnuas_linux_image.py # builds the linux .deb set
cnuasgpu_modules.py # cnuasgpu.ko binary .deb
cnuasnic_modules.py # cnuas_net.ko + cnuas_ib.ko
libcnuasrt.py
cnuas_vswitchd.py
cnuas_cli.py
cnuaslink_cli.py
image/ # `cnuas-tools image ...`
__init__.py
cli.py
builder.py
manifest.py
recipe.py # loads YAML recipes
recipes/
cnuas-vm.yaml
configs/
cnuas.config # kernel-config overlay applied at
# build time on top of x86_64_defconfig
vm/ # `cnuas-tools vm ...`
__init__.py
cli.py
engine.py # abstract VmEngine base class
engine_subprocess.py # uses virsh, qemu-img
engine_libvirt.py # stub for Phase 6.3
cloud_init.py
network.py # mgmt libvirt net definition
state.py
templates/
domain.xml.j2
user-data.yaml.j2
meta-data.yaml.j2
network-mgmt.xml
release/ # `cnuas-tools release ...`
__init__.py
cli.py
github.py # wraps `gh` CLI (deferred, Phase 6.4)
tests/
test_cli.py
pkg/
test_binary.py
test_targets.py
test_cli.py
image/
test_builder.py
test_manifest.py
vm/
test_engine_subprocess.py
test_cloud_init.py
test_network.py
fixtures/
fake_virsh.py
fake_qemu_img.py
4.2 pyproject.toml¶
[build-system]
requires = ["poetry-core>=1.9"]
build-backend = "poetry_core.masonry.api"
[tool.poetry]
name = "cnuas-tools"
version = "0.2.0"
description = "Cnuas tooling: package builds, golden-image generation, VM lifecycle"
authors = ["PacketFive <cnuas@packetfive.io>"]
license = "GPL-2.0-or-later OR MIT"
readme = "README.md"
packages = [{ include = "cnuas_tools", from = "src" }]
[tool.poetry.dependencies]
python = "^3.11"
typer = "^0.12"
rich = "^13.7"
pyyaml = "^6.0"
jinja2 = "^3.1"
[tool.poetry.group.dev.dependencies]
pytest = "^8.3"
pytest-cov = "^5.0"
black = "^24.8"
flake8 = "^7.1"
flake8-bugbear = "^24.8"
flake8-pyproject = "^1.2"
isort = "^5.13"
mypy = "^1.11"
pre-commit = "^3.8"
poethepoet = "^0.30"
[tool.poetry.scripts]
cnuas-tools = "cnuas_tools.cli:app"
[tool.black]
line-length = 100
target-version = ["py311"]
[tool.isort]
profile = "black"
line_length = 100
[tool.flake8]
max-line-length = 100
extend-ignore = ["E203", "W503"]
select = ["E", "W", "F", "B", "C"]
[tool.mypy]
strict = true
python_version = "3.11"
[tool.poe.tasks]
fmt = ["black src tests", "isort src tests"]
lint = ["flake8 src tests", "mypy src"]
test = "pytest -q"
check = ["fmt", "lint", "test"]
4.3 CLI surface¶
Top-level:
cnuas-tools --help
cnuas-tools --version
cnuas-tools pkg ...
cnuas-tools image ...
cnuas-tools vm ...
cnuas-tools release ...
pkg¶
cnuas-tools pkg list # list targets
cnuas-tools pkg build [TARGET ...] # build one or all
cnuas-tools pkg clean
Targets: cnuas-linux-image, cnuas-gpu-modules,
cnuas-nic-modules, cnuas-libcnuasrt, cnuas-vswitchd,
cnuas-cli, cnuas-link-cli. Default: all.
Each target maps to a small Python class subclassing Target with a
single build(ctx) method that returns a BuildResult. Targets
internally invoke existing make/bazel/poetry build commands; the
Python layer adds version stamping, output location, and packaging
into .debs (see pkg/binary.py, pkg/kmod.py, pkg/python_cli.py).
See §5 for full details.
image¶
cnuas-tools image build [--recipe RECIPE] [--out DIR]
cnuas-tools image inspect IMAGE.qcow2 # print manifest
cnuas-tools image verify IMAGE.qcow2 # check sha256
The default recipe is recipes/cnuas-vm.yaml. The build outputs
cnuas-vm-vX.Y.Z.qcow2, its .sha256, and a .manifest.json.
vm¶
cnuas-tools vm up NAME [--gpus N] [--port P]
cnuas-tools vm down NAME
cnuas-tools vm console NAME
cnuas-tools vm ssh NAME [-- CMD ...]
cnuas-tools vm scp [LOCAL] NAME:[REMOTE]
cnuas-tools vm list
cnuas-tools vm lab # vm-a + vm-b + switch
cnuas-tools vm lab-down
cnuas-tools vm destroy NAME [--purge]
Per-VM state lives in ~/.local/share/cnuas/vms/<name>/state.json,
ssh port, libvirt domain name, switch socket, GPU count, image
revision. state.json is the single source of truth; tools read from
it and refresh from virsh dominfo.
release¶
Stubs for now; implementation in Phase 6.4.
4.4 The run() wrapper¶
All external commands go through one helper. This is the seam where we
later swap virsh calls for libvirt-python.
# src/cnuas_tools/run.py
import subprocess
from typing import Sequence
class RunError(RuntimeError):
def __init__(self, cmd, returncode, stdout, stderr):
super().__init__(
f"Command failed ({returncode}): {' '.join(cmd)}\n"
f"---stdout---\n{stdout}\n---stderr---\n{stderr}"
)
self.cmd, self.returncode = cmd, returncode
self.stdout, self.stderr = stdout, stderr
def run(cmd: Sequence[str], *, capture: bool = True,
check: bool = True, env: dict | None = None,
cwd: str | None = None, timeout: float | None = None) -> subprocess.CompletedProcess:
proc = subprocess.run(
list(cmd),
capture_output=capture,
text=True,
env=env,
cwd=cwd,
timeout=timeout,
)
if check and proc.returncode != 0:
raise RunError(cmd, proc.returncode, proc.stdout, proc.stderr)
return proc
No magic, no shell. Tests inject a fake by monkey-patching run at
module level.
5. Package builds¶
5.1 Target overview¶
| Target | What it ships | Build engine | Layer |
|---|---|---|---|
cnuas-linux-image |
linux-image-6.19.0-cnuas+, linux-headers-6.19.0-cnuas+, linux-libc-dev |
make x86_64_defconfig + merge_config.sh configs/cnuas.config + make bindeb-pkg |
kernel |
cnuas-gpu-modules |
pre-built cnuasgpu.ko under /lib/modules/6.19.0-cnuas+/extra/cnuas/ + modules-load.d |
out-of-tree make M=... against linux/ |
kernel module |
cnuas-nic-modules |
pre-built cnuas_net.ko + cnuas_ib.ko + modules-load.d |
same | kernel module |
cnuas-libcnuasrt |
libcnuasdev.so.0, libcnuasrt.so.0, headers (compute backend archive is statically linked) |
existing Makefile under src/cnuasgpu/lib/ |
userspace lib |
cnuas-vswitchd |
switch daemon binary + systemd unit | existing Makefile under src/cnuasswitch/switch/ |
service |
cnuas-cli |
Python wheel + transitive-dep wheelhouse + /usr/bin/cnuas-cli launcher |
poetry build + pip wheel |
tool |
cnuas-link-cli |
same pattern as cnuas-cli from src/cnuaslink/cli/ |
same | tool |
Kernel .debs land under out/kernel/; everything else lands directly
in out/. This separation keeps the smaller component .debs
clearly distinct from the larger kernel artefacts.
5.2 Kernel package set (cnuas-linux-image)¶
Cnuas pins guest VMs to a single Linux kernel built from the linux/
submodule of the Cnuas repo. The submodule is at
PacketFive/linux@cnuas-v6.19.0, upstream Linux v6.19 plus exactly
one Cnuas-specific patch:
--- a/include/uapi/rdma/ib_user_ioctl_verbs.h
+++ b/include/uapi/rdma/ib_user_ioctl_verbs.h
@@ -253,6 +253,7 @@ enum rdma_driver_id {
RDMA_DRIVER_ERDMA,
RDMA_DRIVER_MANA,
RDMA_DRIVER_IONIC,
+ RDMA_DRIVER_CNUAS,
};
This single uapi addition lets cnuas_ib.ko claim its driver
identity. Everything else in the Cnuas kernel stack
(cnuasgpu.c, cnuas_net.c, cnuas_ib.c) builds out-of-tree
against an unmodified kernel.
The kernel .config is reproducible:
make x86_64_defconfigin the submodule.scripts/kconfig/merge_config.sh -m .config tools/src/cnuas_tools/image/configs/cnuas.config.make olddefconfig.
configs/cnuas.config is ~70 lines of explicit CONFIG_*=y/m covering
KVM-guest virtio, networking, RDMA, ext4, cgroups+namespaces, BTF, and
CONFIG_LOCALVERSION="-cnuas". The resolved final .config is
shipped at out/kernel/config-6.19.0-cnuas so users on other
distributions can use it as a baseline.
Build invocation:
ccache is auto-used when present (CC="ccache gcc"). Cold build on
a 12-core box is ~8, 12 minutes; warm ccache rebuild is ~30 seconds.
Output: linux-image-6.19.0-cnuas+_*.deb (~16 MB),
linux-headers-6.19.0-cnuas+_*.deb (~9.5 MB),
linux-libc-dev_*.deb (~1.5 MB).
The + suffix on the kernel release is appended by mainline
scripts/setlocalversion whenever HEAD is not at an upstream
vX.Y tag. Our submodule tip is one commit past v6.19, so the
script appends +, this is the canonical "modified upstream tree"
marker and we embrace it. uname -r inside the guest reads
6.19.0-cnuas+.
5.3 Kernel-module packages (binary, not DKMS)¶
For Cnuas's pre-USENIX release we ship pre-built .ko files in plain
binary .debs rather than DKMS source .debs. The rationale:
- One supported kernel = one supported set of
.kos. DKMS exists precisely to handle the "many kernels per install" case, which we explicitly do not support yet. - Pre-built
.kos install in O(1), no compiler in the guest, no install-time race againstlinux-headers-*apt hooks. - The image stays small (no kernel sources, no
build-essential).
cnuas_tools.pkg.kmod.build_kmod_deb() produces these. For each
target the flow is:
- Locate the built kernel source at
<repo>/linux/(must already haveModule.symvers, i.e.pkg build cnuas-linux-imagewas run first). make -C linux M=<driver_src_dir> modules -j$(nproc).- Stage the resulting
.ko(s) under/lib/modules/6.19.0-cnuas+/extra/cnuas/<name>.ko. - Drop a
/etc/modules-load.d/<deb_name>.conflisting the modules to autoload at boot. - Generate
DEBIAN/controlwithDepends: linux-image-6.19.0-cnuas+ | linux-image-extra. postinstrunsdepmod -a 6.19.0-cnuas+;prermdoes the same.
A future DKMS path is documented as a follow-on: when we want to
support users on non-Cnuas kernels, we add a parallel set of targets
cnuas-gpu-dkms / cnuas-nic-dkms. The current binary-module
path stays as the default.
5.4 Userspace .debs¶
libcnuasrt and cnuas-vswitchd build via their existing component
Makefiles. The tooling wrappers:
cnuas-libcnuasrt, runsmake -C src/cnuasgpu/lib, stageslibcnuasdev.so.*,libcnuasrt.so.*, pluscnuasdev.h,cnuasrt.h,cnuas_compute_ops.h. Symlinks for SONAME + unversioned dev names.cnuas-vswitchd, runsmake -C src/cnuasswitch/switch, stages thecnuas-vswitchdbinary at/usr/bin/and the bundled systemd unit at/lib/systemd/system/cnuas-vswitchd.service.
5.5 Python CLI .debs (cnuas-cli, cnuaslink-cli)¶
Built via cnuas_tools.pkg.python_cli.build_py_cli_deb():
poetry build -f wheelin the source repo.pip wheel -w <wheelhouse>to collect all transitive deps as wheels.- Stage the wheels under
/opt/cnuas/wheels/<pkg>/. postinstcreates/opt/cnuas/venvs/<pkg>viapython3 -m venv,pip install --no-index --find-linksfrom the wheelhouse, and writes a one-line/usr/bin/<cli>launcher.
This avoids Debian's PEP 668 "managed environment" restrictions on system Python while keeping the install offline and reproducible.
6. The golden image¶
6.1 Recipe (recipes/cnuas-vm.yaml)¶
name: cnuas-vm
description: Cnuas VM golden image
version: 0.2.0
base:
source: cloud-image
url: https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img
sha256: <pinned>
disk:
size_gb: 32
packages:
apt:
# Userspace runtime deps only. No DKMS / build-essential /
# linux-headers-generic - we ship pre-built kernel modules.
# No linux-image-generic - we install cnuas-linux-image and
# purge the stock kernel in post_install.
- libgomp1
- libjansson4
- libcurl4
- python3
- python3-venv
- openssh-server
- rdma-core
- libibverbs1
- ibverbs-utils
- perftest
- ethtool
- pciutils
- cloud-init
- kmod
cnuas:
# Kernel first so module .debs find /lib/modules/6.19.0-cnuas+/.
- cnuas-linux-image # 3 .debs (image, headers, libc-dev)
- cnuas-gpu-modules # cnuasgpu.ko (loads at boot via modules-load.d)
- cnuas-nic-modules # cnuas_net.ko + cnuas_ib.ko
- cnuas-libcnuasrt
- cnuas-vswitchd
- cnuas-cli
- cnuas-link-cli
user:
name: cnuas
sudo: "ALL=(ALL) NOPASSWD:ALL"
password: cnuas
ssh_pwauth: yes
# Modules autoload via the modules-load.d files baked into the
# cnuas-*-modules .debs, so modules_load here stays empty (avoids
# double-registration).
modules_load: []
services:
enable:
- ssh
- cnuas-vswitchd
disable:
- unattended-upgrades
- apt-daily.timer
- apt-daily-upgrade.timer
udev:
- file: 90-cnuas.rules
rule: 'KERNEL=="cnuasgpu[0-9]*", MODE="0660", GROUP="cnuas"'
post_install:
- echo "Cnuas golden image (kernel 6.19.0-cnuas+)" > /etc/motd
- mkdir -p /run/cnuas
# Remove the stock Ubuntu 6.8 kernel; we boot 6.19.0-cnuas+ only.
- apt-get -y purge 'linux-image-6.8*' 'linux-headers-6.8*' 'linux-modules-6.8*' 'linux-image-generic' || true
- update-grub2 || update-grub || true
6.2 Builder steps (cnuas_tools.image.builder.ImageBuilder.build)¶
- Pre-flight check that
qemu-img,virt-customize,virt-resize,virt-sparsify,virt-ls,virt-filesystemsare all on$PATH. - Fetch base image if not cached; verify sha256.
- virt-resize --expand
copies the base into out/cnuas-vm-<ver>.qcow2at the target disk size, growing the ext4 root in the same pass. (Plainqemu-img resizeonly grows the qcow2 file; cloud-init's growpart fires only at first boot, but virt-customize never boots, so we must expand the FS up front.) - Stage
.debs underout/work/debs/. Resolvescnuas-linux-imageto all matchinglinux-*.debunderout/kernel/, every other entry to a single<name>_*.debfromout/. - virt-customize as a single invocation. The first
--run-commandis a multi-step shell script that (a) brings up eth0 in the appliance VM (libguestfs on Ubuntu 24.04 does not do this automatically), (b) writes a minimal/etc/resolv.conf, and (c) runsapt-get update,apt-get install <recipe.apt>, thendpkg -iof the staged Cnuas.debs withapt-get -fy installas a fallback. Subsequent--run-commands configure modules-load.d, enable/disable services, install udev rules, edit/etc/ssh/sshd_config, and run post-install commands. - virt-sparsify in place.
- virt-ls /lib/modules picks up the installed kernel release for the manifest.
- Manifest + sha256 sidecars written next to the qcow2.
Key gotchas that drove the v1 design:
virt-customize --networkis not the default on Ubuntu 24.04 libguestfs 1.52 despite the man page; pass it explicitly.- The libguestfs appliance uses SLIRP at
169.254.0.0/16with gateway169.254.2.2and DNS proxy169.254.2.3, not the qemu default10.0.2.0/24. virt-customizewipes/etc/resolv.confbetween--run-commandinvocations, so any apt-network setup must happen inside the same command that does the apt operations.- The Ubuntu cloud image ships with no
/etc/resolv.confat rest (cloud-init writes it at boot).
The whole pipeline takes 4, 8 minutes on a modern workstation
(virt-resize ~2 min, apt update + apt install ~2 min, dpkg + depmod
~1 min, sparsify ~1 min).
Manifest format (cnuas_tools.image.manifest):
{
"manifest_version": 1,
"name": "cnuas-vm",
"version": "0.2.0",
"built_at": "2026-05-18T15:44:13Z",
"host": "cnuas1",
"git_sha": "2953c878f3781285b4541cc49c543db707893a5c",
"base_image": {
"url": "https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img",
"sha256": null
},
"kernel_version": "6.19.0-cnuas+",
"components": {
"cnuas-cli": {"version": "0.1.0", "deb_sha256": "..."},
"cnuas-gpu-modules": {"version": "0.2.0", "deb_sha256": "..."},
"cnuas-link-cli": {"version": "0.1.0", "deb_sha256": "..."},
"cnuas-nic-modules": {"version": "0.1.0", "deb_sha256": "..."},
"cnuas-libcnuasrt": {"version": "0.2.0", "deb_sha256": "..."},
"cnuas-vswitchd": {"version": "0.1.0", "deb_sha256": "..."},
"linux-headers-6.19.0-cnuas+": {"version": "6.19.0-cnuas.1","deb_sha256": "..."},
"linux-image-6.19.0-cnuas+": {"version": "6.19.0-cnuas.1","deb_sha256": "..."},
"linux-libc-dev": {"version": "6.19.0-cnuas.1","deb_sha256": "..."}
},
"image_sha256": "ee3a779796ef6006a096c0ec60d05975c412fa6eecb1915bbeb2742bef..."
}
6.3 Idempotency¶
image build is purely functional: same inputs → same outputs. We
achieve this by:
- Pinning the base image sha256 in the recipe (mandatory for releases; optional during development).
- Pinning the linux/ submodule to a specific commit (currently
PacketFive/linux@cnuas-v6.19.0). - Disabling
unattended-upgrades,apt-daily.timer, etc., before package install so dpkg runs deterministically. - Calling
virt-sparsifyto normalize qcow2 metadata.
7. VM lifecycle¶
7.1 Networking¶
Two networks per running lab:
| Network | Type | Purpose |
|---|---|---|
cnuas-mgmt |
libvirt NAT, 10.42.0.0/24 |
SSH from host, apt, NTP |
cnuas-fabric |
UNIX sockets (/run/cnuas/port_N.sock) |
Cnuas vNIC ↔ switch |
cnuas-mgmt is defined once on the host:
<network>
<name>cnuas-mgmt</name>
<forward mode='nat'/>
<bridge name='cnuas-mgmt' stp='on'/>
<ip address='10.42.0.1' netmask='255.255.255.0'>
<dhcp>
<range start='10.42.0.10' end='10.42.0.99'/>
<host mac='52:54:00:42:00:01' name='vm-a' ip='10.42.0.11'/>
<host mac='52:54:00:42:00:02' name='vm-b' ip='10.42.0.12'/>
</dhcp>
</ip>
</network>
cnuas-fabric is not a libvirt network, it is a directory of
UNIX sockets owned by cnuas-vswitchd, attached to each VM via the
custom -device cnuas-vnic,socket_path=.... The switch is started
separately (see §7.3).
7.2 Per-VM domain XML (template)¶
Rendered from templates/domain.xml.j2:
<domain type='kvm'>
<name>cnuas-{{ name }}</name>
<uuid>{{ uuid }}</uuid>
<memory unit='GiB'>{{ ram_gb }}</memory>
<vcpu placement='static'>{{ vcpus }}</vcpu>
<cpu mode='host-passthrough' check='none'/>
<os>
<type arch='x86_64' machine='q35'>hvm</type>
<boot dev='hd'/>
</os>
<devices>
<emulator>{{ qemu_path }}</emulator>
<disk type='file' device='disk'>
<driver name='qemu' type='qcow2'/>
<source file='{{ disk_path }}'/>
<target dev='vda' bus='virtio'/>
</disk>
<disk type='file' device='cdrom'>
<driver name='qemu' type='raw'/>
<source file='{{ seed_iso }}'/>
<target dev='sda' bus='sata'/>
<readonly/>
</disk>
<interface type='network'>
<mac address='{{ mac }}'/>
<source network='cnuas-mgmt'/>
<model type='virtio'/>
</interface>
<!-- Cnuas vNIC and CnuasGPU devices live in <qemu:commandline>
until upstream libvirt adds direct support for them. -->
<qemu:commandline xmlns:qemu='http://libvirt.org/schemas/domain/qemu/1.0'>
<qemu:arg value='-device'/>
<qemu:arg value='cnuas-vnic,socket_path={{ fabric_socket }}'/>
{% for gpu in gpus %}
<qemu:arg value='-device'/>
<qemu:arg value='cnuasgpu,gpu_id={{ gpu.id }}'/>
{% endfor %}
</qemu:commandline>
<serial type='pty'><target type='isa-serial' port='0'/></serial>
<console type='pty'><target type='serial' port='0'/></console>
<graphics type='none'/>
</devices>
</domain>
7.3 vm up flow¶
- Read
~/.local/share/cnuas/vms/vm-a/state.jsonif it exists. If the domain is already running, return. - Ensure
cnuas-mgmtlibvirt network is defined and active. If not, define + start. - Ensure switch is running on
/run/cnuas/. If not, startcnuas-vswitchd --run-dir /run/cnuas --daemon. - Resolve image path:
~/.local/share/cnuas/images/cnuas-vm-<ver>.qcow2. If not present, error (cnuas-tools image buildfirst). - Allocate per-VM directory
~/.local/share/cnuas/vms/vm-a/. qemu-img create -F qcow2 -b <golden> -f qcow2 disk.qcow2.- Generate cloud-init seed:
user-data.yaml: hostname, SSH public key (from~/.ssh/id_*.pub),ssh_pwauth: yes, growpart, no upgrades.meta-data.yaml: instance-id, hostname.network-config.yaml: DHCP via ens3.genisoimage -o seed.iso -V cidata -r -J user-data meta-data network-config.- Render
domain.xmlfrom template, fill in UUID, MAC, paths, GPU list. virsh define vm-a/domain.xml.virsh start cnuas-vm-a.- Wait up to 60 s for SSH (poll
ssh -o ConnectTimeout=2). - Update
state.jsonwithpid,console_pts,ip,ssh_port.
7.4 vm down¶
The overlay disk is preserved so the VM's own state survives across
restarts. vm destroy --purge deletes the overlay.
7.5 vm ssh and vm scp¶
Discover the VM's IP from state.json (refreshed from
virsh domifaddr when stale), then exec ssh cnuas@<ip> with
sensible default options (-o StrictHostKeyChecking=accept-new).
7.6 vm lab¶
Brings up the full demo:
1. Switch.
2. vm up vm-a --gpus 1.
3. vm up vm-b --gpus 1.
Equivalent to running vm up for both with the right defaults.
7.7 vm console¶
virsh console cnuas-vm-a, same as today. Useful when SSH is
broken; otherwise prefer vm ssh.
8. CI / ADO integration¶
(Phase 6.4, sketched here so this doc reflects the contract.)
| Pipeline stage | Pool | Command |
|---|---|---|
lint |
cnuas-linux |
cd tools && poetry install && poetry run poe lint |
test |
cnuas-linux |
cd tools && poetry install && poetry run pytest |
pkg-build |
cnuas-linux-big |
cnuas-tools pkg build |
image-build |
cnuas-linux-big |
cnuas-tools image build (artefact: qcow2 + manifest) |
lab-up |
cnuas-linux-big |
cnuas-tools vm lab && pytest -q tests/e2e/ |
release |
cnuas-linux |
cnuas-tools release publish $(VERSION) |
Image and .deb artefacts are uploaded to ADO pipeline artefacts and
mirrored to GitHub Releases by release publish.
9. Versioning¶
- Each component repo has its own semver (
cnuas-libcnuasrt→src/cnuasgpu/lib'sSO_VERSION;cnuas-vswitchd→src/cnuasswitch). cnuas-toolscarries its own semver.- The golden image version is set by the user at build time
(
--version 0.2.0) and recorded in the manifest with the resolved component versions. There is no automatic image-version derivation in v1, that's a Phase 6.4 enhancement.
10. Testing strategy¶
10.1 Unit tests¶
tests/ mocks subprocess.run via a fixture that captures the
intended command. Cover:
- Recipe parsing.
- Binary
.deblayout (BinaryDebSpec, file/symlink staging, control, postinst/prerm). - Kernel-module
.deblayout (KModSpec, modules-load.d snippet, depmod hooks). - Image builder pre-flight (refuses cleanly when libguestfs tools are missing).
- Domain XML rendering for various GPU counts. (Phase 6.2e)
- Cloud-init seed contents. (Phase 6.2e)
- State-file lifecycle. (Phase 6.2e)
- Error paths (missing image, libvirt-not-installed, fabric socket missing).
10.2 Integration tests (host)¶
A small tests/integration/ that actually invokes virt-customize
against a tiny synthetic disk (a 64 MiB blank qcow2) to verify the
subprocess calls succeed end-to-end. Skipped if virt-customize is
not installed. Runs in the cnuas-linux-big pool.
10.3 End-to-end tests (nightly)¶
tests/e2e/ builds the real golden image, brings up vm-a, SSHes in,
runs sgemm_smoke and compute_backend_smoke, asserts both pass with
GFLOPS within a tolerance band. Runs in the cnuas-linux-big pool
nightly.
11. Migration plan from current state¶
| Phase | Action |
|---|---|
| 6.2a | Design doc (this file). |
| 6.2b | tools/ skeleton (Poetry, Typer root). Stubs only. ✅ |
| 6.2c | Implement pkg build for libcnuasrt, cnuas-vswitchd, the two Python CLIs (initially with DKMS targets for the kernel modules). ✅ (DKMS targets later replaced, see 6.2f.) |
| 6.2d | Implement image build. Produce the first cnuas-vm-v0.2.0.qcow2. ✅ |
| 6.2e | Implement vm up/down/ssh/console/lab against libvirt. (in progress) |
| 6.2f | Replace DKMS targets with pinned-kernel binary targets: cnuas-linux-image builds the kernel .deb set from the linux/ submodule; cnuas-gpu-modules and cnuas-nic-modules ship pre-built .kos. Recipe rewrite. ✅ |
| 6.3 | Migrate engine_subprocess → engine_libvirt (native libvirt-python bindings) behind the same VmEngine ABC. |
| 6.4 | ADO pipelines (pkg-build, image-build, vm-e2e, release publish). |
| 6.5 | Drop scripts/vm-manager.sh entirely; scripts/boot-vm-kernel-dev.sh (Approach C) stays for kernel-dev iteration. |
12. Deferred decisions¶
Decisions that remain open for this component are carried in the Cnuas Roadmap.
13. Kernel pinning (Phase 6.2f rationale)¶
Cnuas pins guest VMs to a single Linux kernel, 6.19.0-cnuas+,
built from the in-tree linux/ submodule. This is a deliberate
trade-off:
| Pro | Con |
|---|---|
| Users do not build kernels or wait for DKMS on first boot. | Image release cadence is tied to kernel release cadence. |
Driver .kos are matched to the exact kernel they ship with, no unknown symbol surprises. |
Users cannot trivially upgrade their guest kernel without rebuilding the image. |
Reproducibility: same Cnuas commit → same kernel binary → same .ko binaries → same image sha256. |
The Cnuas-specific patch (RDMA_DRIVER_CNUAS) cannot be merged upstream until our USENIX paper publishes, so the linux/ fork is meaningful infrastructure. |
Simpler .debs: no dkms.conf, no install-time compile, no linux-headers-generic in the image. |
Removes the (theoretical) ability for security-conscious users to apt-upgrade to a more recent kernel. |
| Matches how mainline CUDA / NVIDIA HCA stacks ship for production. | More work for us to bump kernel versions later, we'll be doing periodic rebases of the Cnuas patch. |
13.1 The Cnuas kernel patch¶
PacketFive/linux@cnuas-v6.19.0 (commit 84d11a07) is upstream
v6.19 plus this single patch:
--- a/include/uapi/rdma/ib_user_ioctl_verbs.h
+++ b/include/uapi/rdma/ib_user_ioctl_verbs.h
@@ -253,6 +253,7 @@ enum rdma_driver_id {
RDMA_DRIVER_ERDMA,
RDMA_DRIVER_MANA,
RDMA_DRIVER_IONIC,
+ RDMA_DRIVER_CNUAS,
};
The new uapi enum value is what cnuas_ib.ko registers as. Once
the USENIX paper publishes we send the same patch (plus the driver
itself) to linux-rdma@vger.kernel.org for upstreaming and
deprecate this fork.
13.2 Multi-kernel future (optional)¶
If/when we want to support non-Cnuas kernels, the migration is:
- Add a new target
cnuas-gpu-dkms(mirror of the deprecated one) producing source-only.debs. Same forcnuas-nic-dkms. - The recipe gains an optional
package_set:field selecting eitherpinned(current default) ordkms(multi-kernel). - The image-build pipeline either installs `linux-image-6.19.0-cnuas+
- cnuas--modules
(pinned) ordkms + build-essential + linux-headers-generic + cnuas--dkms` (dkms). - The two paths can coexist in a single image with appropriate
conflicts in the
.debmetadata.
Not in v1 scope.
14. Out-of-scope reference, kernel-dev (Approach C)¶
For developers iterating on cnuasgpu.c, the workflow is unchanged from
today: scripts/boot-vm-kernel-dev.sh does a direct-kernel boot using
the in-tree linux/ submodule kernel + src/cnuasgpu/driver/cnuasgpu.ko
built against the same source. No image, no DKMS. Documented in
Cnuas_Driver_Design.md.