Skip to content

firecracker: hot-plug parity for --pci VMs - #218

Merged
CMGS merged 7 commits into
masterfrom
fc/pci-parity
Sep 5, 2026
Merged

firecracker: hot-plug parity for --pci VMs#218
CMGS merged 7 commits into
masterfrom
fc/pci-parity

Conversation

@CMGS

@CMGS CMGS commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Why

#217 gave Firecracker VMs an opt-in virtio-pci transport. This PR makes a --pci Firecracker VM behave like a Cloud Hypervisor VM on every dimension Firecracker can physically support: NIC resize, raw-disk attach/detach, clone --nics override and clone --data-disk. What Firecracker cannot do by construction (vhost-user-fs, VFIO, the memory knobs, Windows, --restore-mode) keeps its explicit rejection, and MMIO Firecracker VMs keep theirs.

What

Five commits, squash-friendly:

  1. review: hoist backend-neutral device-op helpersRunningVMClient, BaseConfig.ResolveExternalVolume, Backend.PrepareCloneDataDisks move out of the Cloud Hypervisor backend unchanged.
  2. review: drive NIC resize through a shared backend driverBackend.NetResizeWith owns the reconcile / add-with-rollback / remove-with-truncate sequence and the record writes; a backend supplies NICDeviceOps (LiveNICs, AddNIC, RemoveNIC, TAPQueues). Devices are matched to record slots by host index (TAP name on CH, eth%d on FC), never by MAC: Firecracker clones keep the snapshot's guest MAC while the record carries fresh ones. ErrEjectPending separates "guest never released it" (host slot reclaimed, error surfaced) from "VMM refused" (nothing touched).
  3. firecracker: hot-plug parity for --pci VMs
    • NetResize, DiskAttach/DiskDetach/DiskList on the Firecracker backend through GET /, GET /vm/config, PUT/DELETE /drives/{id} and /network-interfaces/{id}, all under the VM ops lock with the record reloaded under it; MMIO VMs return the extend package's ErrUnsupportedBackend with the transport named. Hot disks use cocoon_disk_<name> ids (Firecracker ids allow [A-Za-z0-9_] only; hyphenated names are rejected with that reason). TAPs for hot-added NICs are created single-queue, the only shape Firecracker opens.
    • Snapshot and hibernate refuse while a hot-attached disk exists (checked before the pause, --pci VMs only), mirroring Cloud Hypervisor.
    • Clone: --nics N on a --pci snapshot restores every snapshot NIC through network_overrides, then resizes to N with the same driver after the clone's reservation lock is released; --data-disk files are created under the clone's runDir and hot-plugged as the next drive_<n> slots so later snapshots carry them as ordinary sidecar entries. A failed post-clone resize still reports the VM (JSON included) and then the error.
    • Guest side stays the caller's: Firecracker delivers no hot-plug notification, so vm net, vm disk attach/detach and clone print the rescan / stale-node removal commands and return them as hints in --output json (clone output is the VM record plus hints).
    • Docs: README, cli.md, devices.md (new Firecracker section), networking.md, snapshots.md, known-issues.md, vm.md.
  4. doctor: install the cocoonstack fork build of Firecracker--upgrade installs Firecracker from the fork release tag FC_REF (default dev, upstream main plus release CI) verified against its SHA256SUMS and records the build commit, the same way doctor: install the cocoonstack fork builds of cloud-hypervisor and the firmware #215 did for Cloud Hypervisor and the firmware; it is the build the hot-plug and MTU paths are validated on. Upstream v1.16.1 stays the documented floor (its API parser already routes DELETE for drives and network interfaces). install.md follows.
  5. firecracker: clone JSON carries the guest hints and the rescan prints first — from the owner's Codex pass: --output json on clone now embeds the VM record plus hints (additive, vk-cocoon's plain unmarshal is unaffected); the text output prints the PCI rescan before the post-clone setup that names the new interfaces; three doc pages said --format json where the commands register --output. Two Codex items were checked and not taken: Firecracker's guide removes the guest node before the host unplugs, but cocoon runs nothing inside the guest, so it unplugs first (after unmount/down) and prints the stale-node removal — the E2E matrix exercises exactly that order on NICs and disks and devices.md now states it; and a hot-plug PUT/DELETE whose reply is lost behaves as on Cloud Hypervisor (single-shot on a local unix socket, vm inspect shows the live device set for the retry decision).

Hot-path cost: none (no change on VM create, boot or claim; vm net fetches the live device set once per call).

Evidence

Gates on b3e0af0: make fmt-check clean; make lint 0 issues on GOOS=linux and darwin; asl ./... and GOOS=linux asl ./... 0 findings; GOWORK=off go test -race -count=1 ./... 35 ok packages, no FAIL.

Doctor --upgrade in debian:bookworm-slim containers on both architectures: Firecracker dev (commit aa9ca227) installed with sha256 e16bbd38… on arm64 and 5b03ba7e… on x86_64, both equal to the release SHA256SUMS entries; Cloud Hypervisor, ch-remote, firmware and CNI unchanged from #215.

Review round: three independent readers (adversarial correctness, /simplify four lenses, /code style ledger over every touched file). Findings applied: clone re-acquiring the VM ops lock (deadlock on clone --nics), boot-time NIC shrink on Cloud Hypervisor losing the eject (MAC keying), Firecracker clones deleting restored NICs (MAC divergence), multi-queue TAPs Firecracker cannot open, host slot reclaimed after a refused eject, GET /vm/config on MMIO snapshots, Result.Hints leaking into the contract type, dead newTestCH, duplicated runningVMClient, stale disk-package comments. Rejected: constructor placement between type and methods (the repo's documented layout), pushing the clone resize into the backend (no second caller exists).

Hardware E2E (cocoon-test2, isolated root, Firecracker fork dev build v1.18.0-dev, Cloud Hypervisor fork dev build v54.0.0, ghcr.io/cocoonstack/cocoon/ubuntu:24.04, CNI bridge MTU 9000). The same matrix on three arms — Cloud Hypervisor, Firecracker --pci, Firecracker MMIO — each step verified inside the guest:

pr-parity4 on cocoon-test2, 64 checks, all PASS
# Cloud Hypervisor: 26 PASS
  eth0 mtu 9000 | vm net 1->2 | guest sees eth1 | eth1 mtu 9000 | vm net 2->1 | guest back to eth0 only | disk attach | guest sees 64M disk | inspect lists scratch | hibernate refused with disk | disk detach | guest disk gone | hibernate | clone --nics 3 --data-disk | clone guest 3 NICs (5) | clone guest sees extra disk | clone record 3 NICs | clone disk attach (restored VM) | clone inspect lists late (live config after restore) | clone record has extra disk | clone --nics 1 (shrink) | clone shrink record 1 NIC | hibernate clone | re-clone | re-clone keeps 3 NICs + extra disk (guest eth=5) | restore in place
# Firecracker --pci: 31 PASS
  eth0 mtu 9000 | vm net 1->2 | guest sees eth1 | eth1 mtu 9000 | net hint printed | vm net 2->1 | remove hint printed | guest back to eth0 only | disk attach | guest sees 64M disk | inspect lists scratch | hibernate refused with disk | disk detach | guest disk gone | hibernate | clone --nics 3 --data-disk | clone guest 3 NICs (3) | clone guest sees extra disk | clone record 3 NICs | clone disk attach (restored VM) | clone inspect lists late (live config after restore) | clone hint printed | clone record has extra disk | clone --nics 1 (shrink) | clone shrink record 1 NIC | clone shrink hint printed | clone shrink guest 1 NIC | hibernate clone | re-clone | re-clone keeps 3 NICs + extra disk (guest eth=3) | restore in place
# Firecracker MMIO: 7 PASS
  eth0 mtu 9000 | vm net rejected (MMIO) | disk attach rejected (MMIO) | clone --nics rejected (MMIO) | clone --data-disk rejected (MMIO) | plain clone | restore in place

Two earlier runs on intermediate builds caught the multi-queue TAP Firecracker cannot open and the cocoon-disk- id Firecracker rejects; both are fixed in the branch. On Cloud Hypervisor the guest shows five or six eth* names right after a clone because the hot-swapped snapshot NICs disappear only once the guest processes the ACPI ejects (docs/known-issues.md: "cannot wait for guest B0EJ"); the check accepts that on Cloud Hypervisor and verifies the record exactly. That path is untouched by this PR. Full log and the round's write-up: cocoon-specs tests/2026-09-05-fc-hotplug-mtu-parity.md (2026-09-06 addendum).

CMGS added 7 commits September 6, 2026 01:06
RunningVMClient, AppendNetworkConfig, TruncateNetworkConfigs and
NICPersisted move from the Cloud Hypervisor backend to Backend, and
resolveExternalVolume becomes BaseConfig.ResolveExternalVolume, so the
Firecracker backend can share them instead of copying. No behaviour
change; the tests move with the code.
The reconcile / add-with-rollback / remove-with-truncate sequence is
VMM-neutral; only listing live NICs, adding one and removing one differ.
Backend.NetResizeWith owns the sequence and the record writes, and the
Cloud Hypervisor backend supplies those three operations, so the
Firecracker backend can reuse the driver instead of a second copy.
A Firecracker VM created with --pci now resizes NICs, attaches and
detaches raw disks, and takes --nics and --data-disk on clone the way a
Cloud Hypervisor VM does. The backend drives the virtio-pci devices
through GET /, GET /vm/config and PUT/DELETE on /drives and
/network-interfaces under the VM ops lock; MMIO VMs keep the explicit
rejection because only the PCI transport hot-plugs. Hot-attached disks
block snapshot and hibernate like on Cloud Hypervisor, use
cocoon_disk_<name> ids because Firecracker ids allow [A-Za-z0-9_] only,
and hot-added TAPs are single-queue, the only shape Firecracker opens.
A --pci clone restores every snapshot NIC through network_overrides
and resizes to --nics afterwards, once the clone's reservation lock is
released, and hot-plugs --data-disk files as the next drive slots so
later snapshots carry them as ordinary sidecar entries. Firecracker
delivers no hot-plug notification to the guest, so the CLI prints the
rescan and stale-node removal steps and returns them as hints in JSON.
The --pci hot-plug and NIC MTU paths are validated on the fork's dev
release build, so --upgrade now installs it the way it installs the
Cloud Hypervisor and firmware fork builds: from the release tag FC_REF,
verified against the published SHA256SUMS, with the build commit
recorded. Upstream v1.16.1 stays the documented floor.
… first

A --pci clone's --output json now embeds the VM record plus hints, so an
orchestrator learns the rescan/remove steps the way vm net and vm disk
already report them; the text output prints the PCI rescan before the
post-clone setup that names the new interfaces. The docs named the JSON
flag --format; the commands register --output.
@CMGS
CMGS merged commit 1913c72 into master Sep 5, 2026
4 checks passed
@CMGS
CMGS deleted the fc/pci-parity branch September 5, 2026 23:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant