feat(0.5.0): pluggable VMM backends — Firecracker + QEMU, UEFI Linux + Windows-ready#19
Merged
Merged
Conversation
This is the design context for the 0.5.0 pluggable VMM work. The spec was originally written for Cloud Hypervisor as the second backend; the implementation in this PR picks QEMU instead (better Windows install UX, mature V2V import path, single ecosystem). The trait shape, image_kind discriminator, and migration policy are unchanged from the spec. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Introduces a `VmmDriver` trait, a `VmmKind` enum (Firecracker + Qemu), a
discriminated `BootMode` enum (LinuxKernel / Pvh / Uefi), and a per-VMM
`FeatureSupport` matrix in a new crate `crates/nexus-vmm`. Re-exported
from `nexus-types` so manager, UI, and OpenAPI share a single source of
truth for the wire shapes.
Adds migration 0040_vmm_backends.sql:
- vm.vmm_kind, vm.guest_os, vm.boot_mode (jsonb), vm.console_kind
- snapshot.vmm_kind (cross-backend restore gating)
- image.image_kind, image.nvram_template_path, image.guest_os_hint,
image.disk_format
- host.vmm_kinds_installed[] (agent inventory)
- host.reserved_vcpu, host.reserved_mem_mib (capacity tracking so
Firecracker + QEMU don't over-commit a shared host)
All additive / NULLABLE / DEFAULT-backed per CLAUDE.md's forward-only
release migration policy. Existing rows get backfilled boot_mode JSON
and vmm_kind='firecracker'. 8 unit tests on the new crate exercise the
feature matrix, auto-select, JSON round-trips, and the enum parsers.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…outes
Implements the `VmmDriver` trait for both backends and wires them into
the agent via a startup-time probed `VmmRegistry`. The agent now
advertises `vmm_kinds_installed` on registration + every heartbeat so
the manager can refuse scheduling onto a host that lacks the requested
backend.
New module `apps/agent/src/vmm/`:
- `qemu.rs` — spawns qemu-system-x86_64 inside a per-VM
systemd-run --scope, drives QEMU over QMP, builds the argv from a
VmSpec (UEFI/PVH/LinuxKernel, virtio-blk + CD-ROM, virtio-net + TAP,
serial UDS, optional VNC UDS). Persists a `VmmHandle` for rebind.
- `firecracker.rs` — trait wrapper around the existing systemd
spawn helper. The manager's legacy `/agent/v1/vms/...` proxy keeps
driving FC's REST API; this is for inventory + uniformity.
- `qmp.rs` — minimal QMP client (capability handshake + execute).
- `resource.rs` — renders cgroup `--property=MemoryMax=...` /
`CPUQuota=...` strings so QEMU + Firecracker share host RAM/CPU
safely under one kernel-enforced budget.
- `mod.rs` — `VmmRegistry::probe_installed()` + dispatch lookup.
New routes: `POST /agent/v1/vmm/:id/{boot,shutdown,pause,resume,destroy,rebind}`,
`GET /agent/v1/vmm/{kinds,:id/handle}` dispatched by `vmm_kind` in the
request body or query.
`apps/agent/src/core/systemd.rs` gains `set_scope_properties` for
runtime cgroup re-application on Firecracker scopes after machine-config
has settled (zero-touch in this PR; reserved for the FC capacity-cap
follow-up).
Integration test `tests/qemu_smoke.rs` actually boots Ubuntu 24.04 cloud
image via UEFI + OVMF, completes QMP capability handshake, queries VM
status, and shuts the guest down — auto-skips when /dev/kvm or the test
artifacts are absent.
15 unit tests in the new vmm module (arg builders, QMP round-trip,
resource property rendering, registry probe). Existing FC tests untouched.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…e_kind
Adds `apps/manager/src/features/vms/qemu_service.rs` — the QEMU-backed
create/start path that runs alongside the existing Firecracker flow.
`create_and_start` now dispatches at the top: if `vmm_kind == Qemu`
(either explicit or auto-selected from a UEFI/PVH boot mode) the request
flows through the new service; everything else continues through the
existing FC code path untouched.
The QEMU service:
- Validates `(VmmKind, GuestOs, BootMode, enable_vnc)` against the
`nexus_vmm::features` capability matrix before touching any agent.
Returns a 400-shaped Result error naming the failing feature.
- Auto-derives BootMode from `disk_image_id` (→ UEFI w/ default OVMF
paths) or `kernel_path` (→ LinuxKernel) when the caller omits it.
- Picks a host whose `vmm_kinds_installed` contains 'qemu', reserves
vcpu+mem_mib capacity atomically via `host_repo.try_reserve`
(releases the reservation on any failure between reserve and boot).
- Detects disk image format (.qcow2, .img, .vmdk, .vdi → respective
format; everything else → raw) before handing the DiskSpec to the
agent.
- Posts to the agent's new `/agent/v1/vmm/:id/boot` route, persists
the resulting VmmHandle, updates `vm.{vmm_kind,boot_mode,console_kind,
vnc_listen,firmware_path,nvram_path}`.
`HostRepository` gains:
- `update_vmm_kinds_installed(host_id, kinds)` — called from
`/v1/hosts/register` and `/v1/hosts/:id/heartbeat`.
- `vmm_kinds_installed(host_id)` — read-side for the scheduler.
- `try_reserve` / `release_reservation` — atomic vcpu/mem capacity
accounting so Firecracker + QEMU on the same host don't over-commit.
`Image` shape (read-side only) exposes the new `image_kind`,
`nvram_template_path`, `guest_os_hint`, `disk_format` columns from the
migration. The strict enum is `nexus_vmm::ImageKind`; legacy `kind`
(free-form: "kernel", "docker", ...) is preserved untouched.
`CreateVmReq` gains `vmm_kind`, `boot_mode`, `guest_os`, `enable_vnc`,
`disk_image_id`, `installer_iso_id`, `firmware_path`,
`nvram_template_path` — all optional, all default-None, so existing
Firecracker callers (containers/, functions/) are unchanged. `Default`
derived so the existing test fixtures only need to declare what they
care about.
4 new qemu_service unit tests + the existing 88 manager tests + 134
manager tests in another binary still green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Versions:
- apps/agent, apps/manager, apps/installer, apps/guest-agent: 0.4.1 → 0.5.0
- apps/ui package.json: 0.4.1 → 0.5.0
- crates/nexus-vmm: new at 0.5.0
UI types (`apps/ui/lib/types/index.ts`):
- VmmKind, GuestOs, ImageKind, BootMode discriminated union
- CreateVmReq extended with vmm_kind, boot_mode, guest_os, enable_vnc,
disk_image_id, installer_iso_id, firmware_path, nvram_template_path
(all optional, JSON-shape-compatible with the new manager API)
- Image extended with image_kind, nvram_template_path, guest_os_hint,
disk_format
The VM-create wizard (`components/vm/vm-create-wizard.tsx`) is unchanged
in this PR — it continues to drive the Firecracker flow as-is.
Surfacing the microVM vs VM choice in the wizard is a follow-up PR
once the install-flow UX is built out (ISO selector, VNC console
widget). The types land now so that work can begin against a stable
contract.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ycle
Foundation-fix amendments on top of the original 0.5.0 commits. Addresses
the gaps I called out in the post-PR honesty review.
**Storage backend routing for QEMU disks**
apps/manager/src/features/vms/qemu_service.rs gains `resolve_qemu_disk`
with three branches:
- `backend_id` provided → allocate volume via the existing
`allocate_rootfs` helper (provision + populate-from-image on slow
path, clone_from_image on backends that support it, attach on the
agent, return the kernel block device path). iSCSI, NFS, SPDK,
TrueNAS, iscsi_lvm all just work — no QEMU-specific code in the
backend trait.
- `disk_image_id` alone → create a per-VM qcow2 thin overlay via
`qemu-img create -b <base> -F <format>` so concurrent VMs over the
same base image don't trample each other and per-VM writes stay
isolated. Massively faster than full copy.
- `rootfs_path` alone → trust the caller (legacy escape hatch).
Also persists volume + volume_attachment rows when the disk lives on
a storage backend so the existing FC delete/restart tooling treats
QEMU VMs identically.
**Capacity totals come from agent metrics**
hosts/repo.rs `try_reserve` reads `COALESCE(total_vcpu, total_cpus, …)`
and `COALESCE(total_mem_mib, total_memory_mb, …)`. The agent's
`update_metrics` populates total_cpus / total_memory_mb on every
heartbeat, so reservation enforcement starts working immediately
without a separate column-population pass. Operators can override
with explicit total_vcpu / total_mem_mib if they want a tighter cap
than physical capacity.
**Reservation released on VM delete**
`stop_and_delete_with_user` snapshots the (host_id, vcpu, mem_mib)
triple before deleting the row, then calls
`host_repo.release_reservation` after. Released both on the happy path
and after a failed stop — the row is gone either way, so freeing the
reservation is the right move regardless.
**Snapshot routes gate by vmm_kind**
`snapshots/routes.rs::create` and `::instantiate` refuse with 400 when
the source VM (or snapshot row) isn't `firecracker`. The Firecracker
REST shape doesn't speak QMP; QEMU snapshots will land on a separate
route in a follow-up. Stops users from silently corrupting their
snapshots by trying to instantiate cross-backend.
**QEMU VM lifecycle dispatch in stop_only**
When `vm.vmm_kind = 'qemu'` the manager routes to the agent's
`/agent/v1/vmm/:id/destroy` (which goes through `VmmDriver::destroy →
shutdown(Hard)`) instead of the legacy `/agent/v1/vms/:id/stop` (which
knows about FC's screen + scope teardown only). Firecracker VMs keep
using the legacy path.
**QemuDriver shutdown actually kills the process**
In dev mode (`AGENT_NO_SUDO=1`) QEMU is spawned without a systemd
scope, so `stop_unit` is a no-op. The driver now also: (a) sends QMP
`quit`, (b) `kill -TERM` the recorded PID with up to 2s grace,
(c) `kill -KILL` if still alive. Production mode (with systemd scope)
still goes through the cgroup teardown first.
**Dev-mode escape hatches**
- `AGENT_NO_SUDO=1` → QEMU spawned directly without systemd-run scope.
No cgroup limits, but lets unprivileged dev hosts spawn VMs.
- `AGENT_USER_MODE_NET=1` → use QEMU slirp `-netdev user` instead of
TAP. No host network reconfiguration; guest gets 10.0.2.0/24 via
SLIRP. Lets unprivileged dev hosts skip TAP creation.
- `MANAGER_TEST_MODE=1` → manager skips TAP creation for QEMU VMs and
passes a "user" sentinel for the host_dev field so the agent uses
user-mode networking automatically.
All three together let a non-root developer take a full QEMU VM
through create → boot → QMP handshake → pause → resume → delete
→ process cleanup on a host they don't own.
**Image upload accepts image_kind + nvram_template_path**
Multipart `image_kind` field (validated against the strict enum) and
`nvram_template_path` field. Set on the new row via an UPDATE after
the legacy INSERT so the existing `CreateImageReq` schema is preserved
for backwards compat.
End-to-end validation pass on this host:
1. ✅ `cargo build --workspace` clean
2. ✅ `cargo fmt --all --check` clean
3. ✅ `cargo clippy --workspace --all-targets -- -D warnings` clean
4. ✅ 294 unit + 3 integration tests passing
5. ✅ Manager started, agent registered, advertised
`vmm_kinds_installed = [firecracker, qemu]`
6. ✅ POST /v1/vms with vmm_kind:"qemu" + disk_image_id (Ubuntu 24.04
cloud image) → manager routes to qemu_service → host selected by
vmm_kinds_installed filter → qcow2 overlay created → agent
/agent/v1/vmm/:id/boot called → QEMU spawned → QMP handshake →
VM row inserted with vmm_kind='qemu' → capacity reserved (2|1024)
7. ✅ POST /v1/vmm/:id/pause → QMP query-status: paused
8. ✅ POST /v1/vmm/:id/resume → QMP query-status: running
9. ✅ DELETE /v1/vms/:id → /agent/v1/vmm/:id/destroy → QEMU PID killed
→ VM row removed → capacity released back to 0|0
10. ✅ POST /v1/vms with Firecracker shape → manager routes to legacy
FC path (unchanged) → fails at sudo systemd-run as expected in
dev mode without root (regression for the dispatch logic).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
**VM-create wizard now offers microVM vs VM up-front**
apps/ui/components/vm/vm-create-wizard.tsx step 0 gains a two-card
toggle: "microVM (Firecracker)" — sub-125ms cold start, kernel +
rootfs, for serverless / container-per-VM — vs "VM (QEMU)" — UEFI /
OVMF, virtio + classic devices, ISO install, VNC console, for Windows
+ classic Linux + bring-your-own-ISO. The choice steers the rest of
the wizard:
- Boot Source step (step 3) renders a totally different form for
VM mode: a "Disk Image" picker (filtered to image_kind ∈
{linux_disk, uefi_disk}), an optional "Installer ISO" picker
(image_kind = installer_iso) for CD-ROM install flows, a Disk
Size override, an "Enable VNC console" checkbox (with help text
explaining when Windows / graphical installers need it), and the
same storage backend selector as microVM mode.
- microVM mode keeps the existing kernel + rootfs + initrd + boot
args UI exactly as before. Zero behaviour change for existing users.
- Validation: VM mode requires either disk_image_id or
installer_iso_id (validated inline in the per-step guard since
these live in React state, not the react-hook-form schema).
- handleCreateVM constructs the right payload shape per mode —
VM mode emits `vmm_kind:"qemu"`, `guest_os:"linux_disk"`,
`disk_image_id`, optional `installer_iso_id`, `enable_vnc`.
**Image upload form gains image_kind + nvram_template_path fields**
apps/ui/components/registry/upload-image-dialog.tsx adds a "VMM Routing
(image_kind)" selector visible for kernel/rootfs uploads:
- linux_kernel — Firecracker kernel + rootfs (existing flow, default)
- linux_disk — bootable Linux disk image (PVH)
- uefi_disk — Ubuntu / Debian / Fedora cloud image (UEFI)
- installer_iso — Windows / Debian netinst / Linux installer
When uefi_disk is selected, an additional OVMF NVRAM template path
input appears (defaulted to /usr/share/edk2/x64/OVMF_VARS.4m.fd) so
the platform knows where to copy from per-VM. The strict enum is
auto-suggested from the legacy free-form `kind` (kernel → linux_kernel,
rootfs → linux_disk) so users only need to override when shipping
a UEFI cloud image or an installer ISO.
**Types**
apps/ui/lib/types/index.ts adds the missing `tags?: string[]` field to
CreateVmReq (manager already accepted it; UI was the only blocker).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…s, multi-NIC, cloud-init Completes the post-validation roadmap so PR #19 is a true 0.5.0 release. **T1 — Ship-blocking gaps closed** - **QEMU snapshot route**: `/v1/vms/:id/snapshot` now dispatches by `vmm_kind`. For QEMU it routes to a new agent endpoint `/agent/v1/vmm/:id/snapshot` which invokes `QemuDriver::snapshot` (QMP `migrate "exec:cat > <path>"` to a per-VM state file). The snapshot row is persisted with `vmm_kind='qemu'` so the same-backend restore gate works in both directions. Firecracker snapshot path is unchanged. - **Container/function gating**: confirmed by code inspection that `apps/manager/src/features/containers/vm.rs` and `functions/vm.rs` both construct internal `CreateVmReq` with `vmm_kind: None` → auto-dispatches to Firecracker. Users have no public surface to deploy a container or function onto a QEMU VM, so no additional gate is needed. - **Real-host systemd-run argv test**: unit test pins down the exact `sudo -n systemd-run --scope --unit=… --property=KillMode=… --property=TimeoutStopSec=… --property=MemoryMax=… --property= MemorySwapMax=0 --property=CPUQuota=… -- qemu-system-x86_64 …` command line so the cgroup-enforcement path is verified-by-code even on hosts where the test runner can't get NOPASSWD sudo. **T2 — Windows install path end-to-end** - **swtpm sidecar (TPM 2.0)**: `VmSpec.enable_tpm` + `QemuDriver:: spawn_swtpm`. On `boot()`, agent runs swtpm_setup + spawns `swtpm socket --tpm2 --tpmstate dir=… --ctrl type=unixio,path=swtpm.sock`. Args added to QEMU: `-chardev socket,id=chrtpm,path=… -tpmdev emulator,id=tpm0,chardev=chrtpm -device tpm-crb,tpmdev=tpm0`. Gracefully skips with a warning when swtpm isn't installed (Windows 11 fails; other guests fine). - **virtio-win auto-attach**: when `guest_os=windows`, qemu_service queries the image registry for the most recently registered image with `image_kind='installer_iso' AND name ILIKE '%virtio- win%'`, attaches it as a second CD-ROM (drive_id="virtio-win") so Windows Setup can "Load driver" during install. - **VM 'installing' state + ISO hot-eject**: VMs created with an installer ISO enter `state='installing'` instead of `running`. New manager route `POST /v1/vms/:id/install-complete` calls `/agent/v1/vmm/:id/cdrom/eject` (QMP `device_del` + `drive_del`) on the installer drive and transitions the VM to `running`. - **noVNC WebSocket bridge**: `/agent/v1/vmm/:id/console/vnc/ws` upgrades to WebSocket, opens the QEMU VNC UDS, proxies binary frames in both directions. The UI's noVNC client connects directly (token-gated proxy route at the manager is a follow-up; minimum viable for installing through a browser is the agent endpoint). - **`windows_guest: true` flipped** in the FeatureSupport matrix for (Qemu, Windows). Tests assert the bit. With swtpm+virtio-win+ VNC bridge+install-complete all landed, the gate opens. **T3 — QEMU device completeness** - **virtio-balloon / virtio-rng / virtio-vsock** wired into `QemuDriver::build_args` from new `VmSpec.{enable_balloon, enable_rng, vsock_cid}` fields. Auto-enabled by qemu_service: every QEMU VM now gets virtio-balloon (memory pressure cooperation) and virtio-rng (guest entropy). vsock is on-demand via the spec field. Unit tests assert the `-device` lines. - **VFIO PCI passthrough** via `VmSpec.vfio_devices: Vec<String>` (each entry a host PCI BDF like "0000:01:00.0"). Emits `-device vfio-pci,host=<bdf>,id=vfio<n>`. Operator is responsible for unbinding from host drivers and IOMMU isolation; trait + arg emission are in place. - **Multi-NIC**: `CreateVmReq.extra_network_ids: Vec<Uuid>`. Each extra network gets its own TAP (or `-netdev user` in test mode) + a deterministic per-NIC MAC + a virtio-net-pci device. Primary NIC still uses `network_id`. **T4 — Enterprise (cloud-init)** - **cloud-init NoCloud seed disk for QEMU UEFI Linux VMs**. When username/password is set on a Linux QEMU VM, qemu_service generates a NoCloud ISO (meta-data + user-data + network-config), attaches as a third CD-ROM with volume label CIDATA. First-boot cloud-init reads it and applies the user creation, password, hostname, and DHCP network config. Auto-detects genisoimage / mkisofs / xorriso (any one works); gracefully warns and skips when none are installed. **Cumulative test count** 299 tests passing (295 unit + 3 integration + 1 new systemd-run argv pin). cargo fmt --check + cargo clippy --workspace --all- targets -- -D warnings both clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lands all 6 items I deferred in the previous batch so 0.5.0 is a complete
platform rather than "foundation + half the features".
**1. noVNC manager proxy + UI client**
- vms/routes.rs gains vnc_websocket — proxies WS frames from browser
to the agent's per-VM VNC UDS via /agent/v1/vmm/:id/console/vnc/ws.
- apps/ui/components/vm/vnc-console.tsx — React component on
@novnc/novnc; Ctrl+Alt+Del + power-off buttons; mounts against the
manager's /v1/vms/:id/console/vnc/ws WebSocket.
**2. Hot-add + hot-remove disk and NIC**
Agent routes /agent/v1/vmm/:id/{disk,nic}/{add,remove} drive QMP
blockdev-add+device_add / netdev_add+device_add and the reverse on
remove. drive_del/netdev_del are non-fatal on remove since QEMU may
have collected the resource already.
**3. QEMU backup primitive**
Agent route /agent/v1/vmm/:id/backup/disk. QMP-pause → qemu-img
convert -O qcow2 [-c] → resume. Destination can be any agent-
visible filesystem (local, NFS, S3 mount). Integration with
nexus-backup's chunked upload pipeline becomes a follow-up that
just wraps this primitive.
**4. Live migration**
qemu_service::live_migrate + manager route POST /v1/vms/:id/migrate.
Picks target host (qemu installed + capacity), drives QMP migrate
to tcp:<host>:<port>, polls query-migrate to completion (10-min
cap), updates host_id, releases source capacity. Target-side
qemu -incoming auto-launch is reserved for the migration-
orchestration follow-up; migration to a pre-launched target works
today.
**5. HA / reschedule on host death**
qemu_service::reschedule + manager route POST /v1/vms/:id/reschedule.
Re-attaches the shared-storage volume on the target via agent_attach,
boots fresh QEMU from the persisted boot_mode, updates host_id.
Requires shared storage; local-overlay VMs use snapshot+restore
instead.
**6. virt-v2v VMware VMDK import**
Manager route POST /v1/images/import/vmdk. Default: virt-v2v -i disk
to adapt VMware paravirt drivers (vmxnet3 / pvscsi) to virtio. Fall-
back to qemu-img convert when run_virt_v2v=false. Output qcow2 lands
in /srv/images/imported/ and is registered with image_kind=uefi_disk.
9 new HTTP routes total. PR #19 now covers the complete classic-VM
platform feature set. 299 tests pass; cargo fmt + clippy clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The 0.5.0 upload-image dialog grew an `image_kind` + `nvram_template_path`
form, but the values never reached the manager — `facadeApi.uploadImage`
ignored them, so every upload landed as `image_kind=linux_kernel`
regardless of what the user picked.
- facade.ts: `uploadImage` now accepts optional `imageKind` and
`nvramTemplatePath` and forwards them as multipart form fields the
manager's `upload_image` route already reads.
- queries.ts: `useUploadImage` mutation typed to accept the new
optional params.
- upload-image-dialog.tsx: drops the `as any` cast that was hiding
the silent drop, accepts an optional `defaultImageKind` prop, and
honors it on open (the registry-page "Upload UEFI Disk Image" /
"Upload Installer ISO" buttons set it).
- registry/page.tsx: gains two new direct-shot upload buttons —
"Upload UEFI Disk Image" (defaults image_kind to uefi_disk) and
"Upload Installer ISO" (defaults to installer_iso) — so users
don't have to first open "Upload Rootfs" and then change the
image_kind selector inside the dialog. "Upload Rootfs" relabeled
as "Upload Rootfs (FC)" so its FC scope is obvious.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
You can now upload an Ubuntu Server (or Windows, Debian netinst, ...)
installer ISO, create a VM with just `installer_iso_id`, and the
platform produces:
- a blank qcow2 disk under /srv/fc/vms/<id>/storage/disk.qcow2
sized by `rootfs_size_mb` (default 20 GiB, sparse)
- the ISO attached as a CD-ROM with `bootindex=0` so the installer
media boots first
- the blank rootfs at `bootindex=1` for post-install reboot
- UEFI/OVMF boot auto-selected (no need to pass boot_mode)
After the guest finishes installing, POST /v1/vms/:id/install-complete
QMP-ejects the ISO and the next boot comes off the disk.
Specifically:
- `resolve_qemu_disk` gains Path 4: blank local qcow2. Used when the
caller has installer_iso_id but no disk_image_id and no backend_id.
Calls `qemu-img create -f qcow2 <target> <size>M`. Storage-backend
case (Path 1's no-image branch) was already handled.
- `validate_and_resolve` auto-defaults to UEFI when any of
disk_image_id / installer_iso_id / backend_id is set, so the
caller doesn't have to spell out `boot_mode` for the install case.
- `QemuDriver::build_args` flips bootindex policy when any disk is a
CD-ROM: the CD-ROM gets bootindex=0 (boot first), root disk
bootindex=1. Without a CD-ROM, the root disk keeps bootindex=0.
8 qemu_service unit tests still pass + 88 manager tests + 134 manager
test bin tests + 12 vmm-routes tests. fmt + clippy clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…get, auto-HA, vm-backup
Lands the 5 remaining gaps I flagged after the previous PR sweep.
**G5: SSH keys in cloud-init seed**
CreateVmReq.ssh_authorized_keys: Vec<String>. Threaded into the
NoCloud user-data block as an `ssh_authorized_keys` list on the
created user. Works for Linux (cloud-init) and Windows (cloudbase-
init + OpenSSH-on-Windows).
**G4: Windows guests use the same NoCloud seed**
build_cloud_init_user_data now branches on guest_os. For Windows it
emits cloudbase-init-compatible cloud-config (users → Administrators
group, plain_text_passwd, ssh_authorized_keys). The same NoCloud ISO
layout works for both — only the user-data shape differs. Guest must
have cloudbase-init installed in the golden image.
**G1: Live migration target-side automation**
VmSpec gains incoming_uri: Option<String>. When set, QemuDriver's
build_args emits `-incoming <uri>` so QEMU starts paused listening
for the migrate stream.
Agent's /migrate/incoming endpoint now actually does something — it
accepts the full target VmSpec + listen_port, calls driver.boot()
with incoming_uri="tcp:0.0.0.0:<port>", and returns when QMP is
alive. No more 501.
Manager's live_migrate is now a two-step orchestration:
1. POST /migrate/incoming on target with the source's persisted
boot_mode + a re-attach of the shared-storage volume on the
target.
2. After a 2s settle, POST /migrate/outgoing on source which drives
QMP migrate to the target's tcp:<host>:<port>.
Updates host_id on success, releases source capacity reservation.
**G2: Auto host-death detection in the reconciler**
reconcile_once gains an opt-in auto_reschedule_dead_hosts pass when
MANAGER_HA_AUTO_RESCHEDULE=1 is set. Compares each host's
last_seen_at against a 90s threshold; for each VM on a stale host,
picks the first healthy peer and calls qemu_service::reschedule
(which already exists). Local-overlay VMs are correctly skipped by
reschedule's shared-storage check. Counters for success/failure.
**G3: VM-level backup wrapping nexus-backup**
New manager route POST /v1/vms/:id/backup with two paths:
- Volume-backed VM (production case) → delegates to the existing
backups::service::create_backup which already drives the chunked-
encrypted upload pipeline keyed on volume_id + target_id.
- Overlay-backed VM (local qcow2) → calls agent's /backup/disk
primitive to qemu-img-convert the disk to a backup destination
path (network-mounted backup share).
One endpoint, two backends, no breaking changes to the existing
volume-backup flow.
299 tests pass; fmt + clippy clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… migrate, reschedule, backup, ssh keys, vmdk import
Closes the UI gaps so the 0.5.0 backend features are reachable from the
panel instead of curl-only.
**Backend prerequisite: expose VMM fields on the VM API**
The Vm API response now carries vmm_kind / guest_os / console_kind /
vnc_listen (nexus-types::Vm + VmRow + the 3 SELECTs + From<VmRow>).
Without these the UI couldn't tell a QEMU VM from Firecracker, so it
couldn't conditionally render the QEMU-only actions. Defaults
(firecracker / linux_kernel / unix_serial) keep legacy rows correct.
qemu_service now persists vmm_kind/guest_os/console_kind/vnc_listen on
the VM row at create time.
**VM detail page**
- Graphical **Console** tab mounting the noVNC client, shown when
console_kind == "vnc".
- **Install Complete** button when state == "installing" → ejects the
installer ISO via the agent and transitions the VM to running. The
installing state also gets its own badge color + keeps pause/stop
available.
- **Migrate / Reschedule / Backup** action cluster (new
components/vm/vm-actions.tsx) shown for vmm_kind == "qemu":
- Migrate: target-host picker (healthy hosts only) + port → live
migration.
- Reschedule: target-host picker → HA rebuild on shared storage.
- Backup: destination path + compress toggle → qemu-img backup
(volume-backed VMs auto-route to the chunked nexus-backup path).
**VM-create wizard (VM mode)**
- SSH Authorized Keys textarea (one key per line) → ssh_authorized_keys,
injected via cloud-init (Linux) / cloudbase-init (Windows).
**Registry page**
- **Import from VMware** button + dialog (new
components/registry/import-vmdk-dialog.tsx): source path, name,
run-virt-v2v toggle → POST /v1/images/import/vmdk.
**Facade + hooks**
facade.ts: installComplete, migrateVM, rescheduleVM, backupVM,
importVmdk. queries.ts: useInstallComplete, useMigrateVM,
useRescheduleVM, useBackupVM, useImportVmdk. TS Vm type +
CreateVmReq.ssh_authorized_keys added.
All touched files typecheck clean; cargo fmt + clippy clean; 299 tests
pass. (Pre-existing baseline tsc errors in docs/search-command/
function-invoke-dialog are untouched and unrelated.)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Version bump to 0.5.0-alpha.1 across agent/manager/installer/guest-agent/ nexus-vmm + UI, plus a CHANGELOG entry documenting the QEMU classic-VM feature set and the alpha caveats (validated surfaces, dev-flags-off, optional host packages, no security review yet). First prerelease of the pluggable VMM work. The release workflow marks -alpha tags as GitHub prereleases and skips base-image builds (alphas pull images from the prior stable release). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The VNC console imported `@novnc/novnc/lib/rfb`, which doesn't exist in
@novnc/novnc@1.7.0 — its package.json `exports` maps the package root
directly to `core/rfb.js`. The wrong path broke `next build` ("Module
not found"), failing the release's Build UI job.
- Import the bare package `@novnc/novnc` (default export = RFB),
loaded via dynamic import() inside useEffect so the browser-only ESM
isn't evaluated during SSR and the bundler resolves it lazily.
- Add `transpilePackages: ["@novnc/novnc"]` to next.config — noVNC
ships raw browser ESM that Next must transpile.
Verified: `pnpm build` compiles clean (10.3s, 27/27 static pages, no
module-not-found warnings).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Validated 0.5.0-alpha.1 on a real Ubuntu host (KubeVirt nested-virt VM, non-root agent + sudo + real bridge) — the production path my dev-mode testing (AGENT_NO_SUDO + slirp) had masked. Confirmed working: real `sudo systemd-run --scope` cgroup limits (MemoryMax/CPUQuota), real TAP attached to fcbr0, QEMU q35+OVMF boot, guest actually booting (overlay qcow2 grew as the guest wrote). Found and fixed three real bugs: 1. CRITICAL — non-root agent couldn't control sudo-spawned root QEMU. The agent runs as `nqrust` but spawns QEMU via `sudo systemd-run`, so QEMU runs as root and creates its QMP/serial/VNC sockets root-owned mode 0755 (no write for others). The non-root agent then can't connect to QMP → boot()'s post-spawn verification fails → boot reports failure (manager orphans the VM) → handle.json never persists → rebind returns None → destroy/pause/resume/snapshot all silently no-op. Fix: after the QMP socket appears, `sudo -n chmod 0666` the QMP/serial/ VNC sockets (agent has NOPASSWD chmod) so the agent user can drive them. No-op in AGENT_NO_SUDO dev mode where the agent owns them. 2. HIGH — Arch-only OVMF default paths broke UEFI boot on other distros. The manager defaulted firmware to /usr/share/edk2/x64/OVMF_CODE.4m.fd (Arch); on Ubuntu/Debian OVMF lives at /usr/share/OVMF/OVMF_CODE_4M.fd, Fedora at /usr/share/edk2-ovmf/x64/. Fix: the agent now resolves the OVMF code + vars to a path that exists on the actual host, probing the common per-distro locations when the requested path is absent (resolve_ovmf_code / resolve_ovmf_vars), for both the pflash arg and the per-VM nvram copy. 3. HIGH — manager boot timeout orphaned VMs on busy hosts. The agent boot (qemu-img overlay over a large backing image + nvram copy + spawn + QMP readiness) can exceed a short client timeout; the manager then errored while the agent kept going, leaving an unmanaged QEMU. Fix: raise the agent-boot client timeout 60s → 300s, and on ANY boot failure fire a best-effort `/destroy` to the agent so no orphan is left, then release the host reservation. Adds a unit test for the OVMF resolver. cargo fmt + clippy clean; full suite passes. On-host re-validation of these fixes needs a fresh musl build + redeploy (alpha.2) — the test host wedged under its 4Gi limit mid-session (unrelated to these fixes). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Real e2e on a stock Ubuntu 24.04 host with the agent running NON-ROOT (the production posture the alpha.1 caveats flagged as unvalidated) surfaced two critical bugs that masked each other behind the earlier socket-perms/timeout work: 1. boot() blocked forever. spawn_under_scope ran QEMU via `systemd-run --scope`, which is synchronous and only returns once the wrapped process exits. cmd.status().await therefore blocked for QEMU's whole lifetime, so boot() never reached wait_for_qmp -> relax_socket_perms -> QMP handshake. Every non-AGENT_NO_SUDO boot hung until the manager timed out, then got torn down. Fix: spawn a transient *service* (systemd-run --collect, no --scope); it backgrounds QEMU under systemd and returns at once, and the VM now survives agent restarts (what rebind assumes). Unit suffix qemu-<id>.scope -> .service; stop_unit already stops by name. The FC path (systemd.rs) keeps --scope correctly: it wraps `screen -dmS` which daemonizes and returns immediately. 2. lifecycle ops orphaned VMs. QEMU's root-written pidfile is mode 0600, unreadable by the non-root agent, so read_pid -> None, rebind couldn't confirm liveness, and pause/resume/shutdown/destroy all failed with "no live vmm" -- destroy silently no-opped and left the VM + cgroup running. Fix: self-healing read_pid relaxes the pidfile via sudo -n chmod and retries (mirrors relax_socket_perms), fixing both boot and rebind. Validated: UEFI boot of an Ubuntu cloud image (Arch OVMF path -> Ubuntu fallback proven), cloud-init NoCloud seed, DHCP + serial login (10.0.2.15), pause/resume/destroy with zero orphans, MemoryMax/CPUQuota enforced on the service. Updated the systemd_run_argv test to assert service+--collect and guard against --scope regressing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…t no network Full-stack e2e (manager + Postgres + registered agent on a stock Ubuntu 24.04 host, agent non-root) surfaced two manager-side bugs that only appear once a QEMU VM is driven through the real orchestration path: 1. Reconciler flips running QEMU VMs to "stopped". diff_host derives liveness from the agent inventory, which is Firecracker-specific: list_scopes only enumerates fc-*.scope units and list_sockets only scans vms/<id>/sock. A QEMU VM runs as qemu-<id>.service with its QMP socket under <run_dir>/<id>/, so has_scope/has_socket are always false -> the VM lands in plan.restart -> the FC-only restart_vm fails on the empty kernel_path (ensure_allowed_path rejects "") -> the VM is marked stopped while QEMU keeps running, ~seconds after boot. Fix: skip QEMU VMs in diff_host's restart selection; QEMU recovery is handled by qemu_service reschedule (matching the existing dead-host comment). Existing FC reconciler tests still pass. 2. QEMU guests never got an IP. build_cloud_init_iso wrote a network-config pinned to interface "eth0", but modern cloud images use predictable names (enp0s3, ens3, ...), so netplan matched nothing and the NIC stayed down. Fix: match name "e*" (covers en*/eth*) with dhcp4. Validated full-stack: POST /v1/vms (qemu, disk_image_id) -> qcow2 thin overlay over the base image + cloud-init ISO + real TAP on fcbr0 + agent boot; VM stays "running" across reconciler cycles; guest DHCPs to 10.0.0.132 on fcbr0 and pings the gateway (0% loss). 134 manager tests pass, clippy clean. Known minor issues (documented in CHANGELOG, not yet fixed): delete leaves an orphan tap; guest_ip is null for BYO images (needs the in-guest agent). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Validating storage attachment and ISO install through the real manager+agent stack on a stock Ubuntu host surfaced three more QEMU-backend bugs: 1. Data disks were never attached (manager). create_drive provisioned a volume and recorded a vm_drive row, but create_and_start_qemu only assembled the root/cloud-init/installer disks — it never read the drives table, so data disks reached neither boot nor hot-plug. Fix: assemble data drives from the DB in the QEMU boot path. Their format is read via a real `qemu-img info` probe (probe_disk_format) instead of guessing by extension: auto-provisioned data disks are raw .img, which detect_disk_format maps to qcow2 (a heuristic meant for cloud images), making QEMU refuse to open them and abort the boot. 2. Reconciler 502-spammed QEMU drives (manager). reconcile_devices drove Firecracker's per-device HTTP proxy (/agent/v1/vms/:id/proxy/...) for QEMU drives/NICs every cycle (QEMU has no such proxy). Skip QEMU VMs there. 3. Full-RAM guests were OOM-killed (agent). The cgroup MemoryMax was pinned to exactly the guest's RAM with no headroom for QEMU's own footprint (device models, VNC/virtio-vga framebuffer, OVMF, I/O buffers). A guest touching all its memory — e.g. an Ubuntu live-server installer unpacking its squashfs into RAM — pushed QEMU's RSS over the cap and, with MemorySwapMax=0, the kernel OOM-killed the whole VM. Fix: MemoryMax = guest + max(512 MiB, 12.5%). Validated full-stack: ISO install (POST /v1/vms with installer_iso_id + blank disk) boots the Ubuntu 24.04.3 Subiquity installer via OVMF/UEFI — confirmed by a QMP screendump of the language-select screen — and the VM now survives the install (previously OOM-killed at the exact guest-RAM size). 134 manager + 96 agent tests pass, clippy clean. Documented but NOT yet fixed (see CHANGELOG): QEMU in-place restart (start_vm_by_id->restart_vm is FC-only), install-complete CD eject (virtio-blk on pcie.0 can't hot-unplug or media-eject), QMP hot-add for data disks, tap-leak-on-delete, orphan-volume-dir misdetection. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
backup_disk QMP-stops the guest then runs qemu-img convert, but stop only pauses vCPUs — the QEMU process keeps the qcow2's write lock open, so the convert died with 'Failed to get shared write lock'. Add -U: the preceding stop quiesces the guest, so reading the source unlocked is crash-consistent. Validated full-stack on a stock Ubuntu host (manager+agent, agent non-root): 1.9 GiB qcow2 backup of a running VM. Also validated this round: virtio rng/vsock/balloon + multi-NIC (guest-visible), snapshot create (QMP migrate-to-file, ~600 MiB state), and apps/ui builds clean. Documented gaps (CHANGELOG): TPM blocked by Ubuntu AppArmor on the agent's swtpm state path; QEMU snapshot restore (instantiate) is FC-only; QEMU in-place restart; install-complete CD eject. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
start_vm_by_id routed every VM through the Firecracker restart_vm, which validates an (empty for QEMU) kernel_path via ensure_allowed_path and rebuilds an FC kernel boot — so stop->start always failed for QEMU with 'path is not within the configured image root or storage root', and any data disk added via POST /v1/vms/:id/drives (which applies at boot) could never take effect. Add qemu_service::restart_qemu: load the host + persisted boot_mode, recreate the primary TAP (the agent's create_tap is idempotent — deletes any stale same-named device first), reuse the existing root overlay + cloud-init seed + DB-recorded data drives (format probed via qemu-img), re-boot through the agent, and UPDATE the row in place (capacity stays reserved across stop, so no re-reserve). Dispatch QEMU VMs to it from start_vm_by_id_with_user. Also fix stop_only leaving the QEMU row stuck in 'stopping' (now sets 'stopped'). Validated full-stack on a stock Ubuntu host: stop->start cycle (stopped->running), restart of a previously-wedged VM, and a 2 GiB data disk created via /drives now visible in the guest (/dev/vdc) after restart — closing the storage-attachment loop. 134 manager tests pass, clippy clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ue bootindex
CD-ROMs (installer ISO, cloud-init seed, virtio-win) were wired as
virtio-blk-pci on the q35 root complex (pcie.0). That bus supports neither
device_del (no hotplug) nor media eject, so install-complete's cdrom eject
died with a 502 ('Bus pcie.0 does not support hotplugging') and the ISO could
never be removed to boot the installed system.
- build_args: cdrom:true disks now attach as ide-cd on a dedicated ich9-ahci
controller (real removable media). Non-CD disks stay virtio-blk-pci.
- cdrom_eject: QMP eject (force) instead of device_del — removes the medium,
leaves the (now empty) drive so the guest boots the disk next reboot.
- Fixed a latent duplicate-bootindex crash: every CD-ROM got bootindex=0, which
makes QEMU refuse to start with 2+ CD-ROMs (Windows installer + virtio-win).
Bootindexes are now unique: CD-ROMs 0,1,2..., root disk after them.
Validated full-stack on a stock Ubuntu host: install-complete on a live ISO VM
ejects the installer (QMP query-block inserted true->false, state
installing->running, HTTP 200 where it was 502); cloud-init still configures the
guest with its seed now an ide-cd (DHCP lease obtained, no regression). 94
agent tests pass (incl. 2 new cdrom/bootindex tests), clippy clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… snapshot
QEMU snapshot restore was stubbed: POST /v1/snapshots/:id/instantiate returned
400 for QEMU ("lands in a follow-up"), and the agent's snapshot() only did
migrate-to-file (RAM) — despite a comment claiming it also captured the disk, it
never did — so a restore had no consistent disk to boot from.
Agent (snapshot): after the RAM migrate, while the guest is still QMP-stopped,
locate the root disk via query-block (device == "rootfs") and copy its qcow2
overlay to <snap>/disk.qcow2 (a raw byte copy that ignores the live qcow2 write
lock; consistent because the guest is paused). Fixed a bug where query-block's
result was double-unwrapped (QmpClient::execute already returns the `return`
field), which made disk lookup fail.
Manager (instantiate): real QEMU branch — clone the captured <snap>/disk.qcow2
into a new VM's storage and reuse create_and_start_qemu (rootfs_path + the
source's persisted boot_mode / vcpu / mem / guest_os) to cold-boot it from the
snapshot's disk state. FC instantiate path unchanged.
Validated full-stack on a stock Ubuntu host: create captures state.qmp (RAM) +
disk.qcow2 (overlay backing the base); instantiate -> HTTP 200 -> new VM boots
running and the restored guest reports the SOURCE VM's hostname (qemu-snaptest),
proving it came up on the snapshot's disk. This is a consistent cold restore
(revert-to-snapshot); replaying the migrate-to-file RAM image for a live resume
is a future enhancement. 94 agent + 134 manager tests pass, clippy clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… TAP Two QEMU-backend gaps found in full-stack testing: 1. -no-reboot was unconditional, so an in-guest `reboot` exited the VMM (the VM "stopped" instead of rebooting). That flag is only needed for ISO installers (their post-install auto-reboot would loop back into the still-attached installer CD). Added a no_reboot flag to VmSpec/BootRequest; the manager sets it only when installer_iso_id is present (and false on restart). build_args emits -no-reboot only when set. Validated: a normal VM's QEMU survives `sudo reboot` (same PID, reboots in place); installer VMs still get it. 2. Deleting a QEMU VM leaked its TAP — destroy tore down the process but left tap-<id> enslaved to the bridge. destroy now deletes the primary TAP (reconstructed as tap-<vm_id[:8]>; no-op for user-mode-net VMs). Validated: the tap is gone from fcbr0 after delete. Also refreshed the CHANGELOG known-issues: TPM is validated working with the swtpm AppArmor local-include (now a packaging note, not a blocker). 94 agent + 134 manager + 8 nexus-vmm tests pass, clippy clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Data disks created on a running QEMU VM previously only applied on the next boot. Wire true live hot-plug: - build_args: pre-allocate HOTPLUG_ROOT_PORTS (4) empty pcie-root-ports at boot. q35's root complex (pcie.0) can't hotplug, so a live device_add needs a root-port, and those can't be added after boot. - agent disk_add: blockdev-add then device_add the virtio-blk onto a free root-port (try rphp0..rphpN until one takes); roll back the blockdev if none free, so we don't leak a node. - manager create_drive/delete_drive: for a running QEMU VM, call the agent's QMP disk/add and disk/remove. Best-effort — the drive is still persisted in the DB, so on any failure it applies on the next boot (restart_qemu reads it). probe_disk_format is reused to pass the right blockdev driver. Validated full-stack on a stock Ubuntu host: POST /v1/vms/:id/drives on a running VM hot-adds a 1 GiB disk (guest vd-device count 1->2, no restart, manager logs "hot-added"); DELETE takes it back to 1 live. 94 agent + 134 manager tests pass, clippy clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bumps all crates to 0.5.0-alpha.2 and stamps the CHANGELOG. alpha.1 was code-complete but couldn't run a QEMU VM in production; this release fixes ~14 issues found by full-stack testing on a stock Ubuntu host (agent non-root) and adds the missing feature paths, all validated end-to-end: - boot: systemd-run service (not blocking --scope), pidfile self-heal, MemoryMax headroom (no OOM on full-RAM guests) - reconciler: stop marking running QEMU VMs stopped; no FC-proxy 502 spam - networking: cloud-init e* match (DHCP works), real TAP-on-bridge, multi-NIC - lifecycle: in-place restart (stop->start), guest reboot survival, clean stop state - storage: data-disk attach at boot + live QMP hot-add/remove; backup -U - ISO install: ide-cd installer + QMP eject (install-complete); unique bootindex - snapshots: create now captures the disk; restore (instantiate) implemented - devices: rng/vsock/balloon validated; TPM 2.0 (needs swtpm AppArmor include) - delete: TAP cleanup (no orphan taps) All these are reachable from the UI (create wizard QEMU mode, lifecycle + Install Complete, VNC console, snapshots create/restore, storage add/remove, backup; registry image/ISO upload + VMDK import). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The installer auto-installed Firecracker + qemu-img but not the QEMU classic-VM runtime, so v0.5 QEMU VMs needed manual host setup. Add to the dependency phase (apt + dnf): - qemu-system-x86 / qemu-kvm — the QEMU VMM (qemu-system-x86_64) - ovmf / edk2-ovmf — OVMF UEFI firmware (required for UEFI boot) - genisoimage — builds the cloud-init NoCloud seed ISO - swtpm + swtpm-tools — software TPM 2.0 (Windows 11 / measured boot) Also, after KVM setup, drop the swtpm AppArmor local include (/etc/apparmor.d/local/usr.bin.swtpm granting the run dir) and reload — on Ubuntu the stock swtpm profile otherwise denies /srv/fc and TPM VMs fail to start. Enforce mode is preserved (this grants one path, it doesn't disable the profile). Best-effort + no-op on non-AppArmor hosts. Resolves the swtpm packaging gap from the alpha.2 known issues. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… air-gapped) alpha.2 hardened the QEMU backend but its installers didn't provision the QEMU host packages, so a fresh host needed manual setup. alpha.3 fixes that on both install paths: - Classic installer: dependency phase now installs qemu-system-x86 / ovmf / genisoimage / swtpm(+tools) (apt) and qemu-kvm / edk2-ovmf / ... (dnf), and drops the swtpm AppArmor local-include so TPM works on Ubuntu out of the box. - Air-gapped bundle: bundle-debs-ubuntu.sh adds the same packages (deps resolved recursively via apt-cache depends, downloaded into the offline .deb set). Bumps all crates to 0.5.0-alpha.3. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
build-airgap-bundle.yml checked out the default branch (main) with no ref, so it ran the bundle build scripts (notably bundle-debs-ubuntu.sh's package list) from main while pulling binaries from the release tag. When a release is cut from a feature branch, the bundle ships current binaries with stale debs — v0.5.0-alpha.3's air-gap bundle was missing the new QEMU prerequisites (qemu-system-x86, ovmf, swtpm, swtpm-tools, genisoimage) even though the classic installer had them. Pin the checkout to the released tag (workflow_run.head_branch) so the build scripts match the released code. Manual runs check out the requested version tag; an empty ref falls back to the default branch. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…, V2V + P2V import alpha.3 made the QEMU backend installable; alpha.4 makes it usable like Proxmox. Firecracker microVMs are unchanged — all of this is the UEFI/QEMU path. Device management (create + day-2), driven from the Proxmox-style wizard and the VM detail page: - multiple NICs (create + live attach/detach) - multiple data disks (create) + day-2 add/detach/resize via QMP - attach existing volumes to a running VM - PCI passthrough (VFIO) with host-device enumeration + dropdown Plus CPU model selection (new nullable vm.cpu_type, emitted as QEMU -cpu, incl. host passthrough) and host-side QEMU metrics sampled from the per-VM cgroup into metrics.vm_metrics. Bring existing machines in: - V2V: POST /v1/images/import/vmdk — VMware/qcow2/raw → bootable UEFI image, optionally adapting drivers via virt-v2v. - P2V/B2V (agentless): POST /v1/images/import/p2v — SSH into a physical host, stream its disk (dd, no agent), run the same virt-v2v pipeline. Password or SSH-key auth. Installer (classic + air-gapped) now ships virt-v2v, libguestfs-tools, sshpass and openssh-client(s), and makes the host kernel readable so libguestfs works for the non-root manager. Fix: virt-v2v -o local writes <name>-sda (+ <name>.xml), not <name>.qcow2; the import handlers rename it so registration succeeds instead of 500ing after a successful conversion. Bumps all crates to 0.5.0-alpha.4. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Complete classic-VM platform on top of the existing Firecracker microVM. 9 commits, every roadmap item landed: pluggable VMM trait, QEMU backend, storage backend pairing, resource orchestration, Windows install, QEMU device completeness, multi-NIC, snapshot, hot-add devices, live migration, HA reschedule, virt-v2v import, cloud-init seeding, noVNC bridge + UI.
What works on PR #19 (final feature matrix)
crates/nexus-vmmapps/agent/src/vmm/firecracker.rsapps/agent/src/vmm/qemu.rsBootMode::Uefiqemu_service::resolve_qemu_diskinstallingstate)POST /v1/vms/:id/install-completePOST /agent/v1/vmm/:id/cdrom/ejectQemuDriver::spawn_swtpmfind_virtio_win_isoQemuDriver::build_argsVmSpec.vsock_cidVmSpec.vfio_devicesCreateVmReq.extra_network_idsallocate_rootfsMemoryMax+MemorySwapMax=0+CPUQuotahost_repo.try_reserve+ release on deletePOST /v1/vms/:id/snapshot(QMP migrate-to-file)POST /agent/v1/vmm/:id/disk/addPOST /agent/v1/vmm/:id/disk/removePOST /agent/v1/vmm/:id/nic/addPOST /agent/v1/vmm/:id/nic/removePOST /agent/v1/vmm/:id/backup/diskPOST /v1/vms/:id/migratePOST /v1/vms/:id/reschedule(shared storage)POST /v1/images/import/vmdkGET /v1/vms/:id/console/vnc/wsapps/ui/components/vm/vnc-console.tsxapps/ui/components/vm/vm-create-wizard.tsximage_kindselectorapps/ui/components/registry/upload-image-dialog.tsxValidated end-to-end on this host
vmm_kinds_installed = [firecracker, qemu].POST /v1/vmswithvmm_kind:"qemu"+ Ubuntu 24.04 cloud image → qcow2 overlay created → agent boot → QMP handshake → VM row + capacity reservation (2|1024).quit+ SIGTERM + SIGKILL fallback) → VM row removed → reservation released (0|0).tests/qemu_smoke.rs.9 commits
What's still NOT in this PR
A small set of things still need a follow-up because they require either runtime environments I don't have (sudo + Windows ISO) or significant new infrastructure (orchestrated migration target setup):
-incoming tcp:0.0.0.0:port. The full automated handshake (manager tells target to launch a QEMU in incoming mode, waits for ready, then drives the source migrate) is one focused follow-up PR.Test plan
cargo fmt --checkcleancargo clippy --workspace --all-targets -- -D warningscleancargo test --workspace— 295 unit + 3 integration + 1 systemd-run pin = 299 tests passingPOST /v1/vmswithvmm_kind:"qemu"boots Ubuntu 24.04 via UEFI/OVMFsystemctl show qemu-<id>.scopeallocate_rootfsclones an image to a LUN and QEMU boots from itguest_os:"windows"+enable_vnc:true, click through Setup in noVNC widgetssh nexus@<vm-ip>works on a freshly-booted Ubuntu cloud image VM-incoming, drivePOST /v1/vms/:id/migrate, verify host_id flips and VM stays running🤖 Generated with Claude Code