Add QEMU microvm hypervisor backend - #376
Conversation
-->
✱ stlc build✅ go code · compare
✅ typescript code · compare
Diagnostics: 💡 0 new / 5 total note
Build metadata
This comment is auto-generated by stlc and is kept up to date as you push. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Microvm switch skips constraint checks
- Restore and fork now validate stopped-snapshot metadata against create-time qemu-microvm constraints before any payload or metadata mutation, so incompatible switches fail early with ErrInvalidRequest.
Or push these changes by commenting:
@cursor push 7810766d9a
Preview (7810766d9a)
diff --git a/lib/instances/snapshot.go b/lib/instances/snapshot.go
--- a/lib/instances/snapshot.go
+++ b/lib/instances/snapshot.go
@@ -273,6 +273,10 @@
if err != nil {
return nil, err
}
+ targetMachineType, err := m.resolveSnapshotTargetMachineType(rec.StoredMetadata, targetHypervisor)
+ if err != nil {
+ return nil, err
+ }
target, err := m.cancelAndWaitCompressionJob(ctx, m.snapshotJobKeyForSnapshot(snapshotID))
if err != nil {
@@ -303,10 +307,7 @@
restored.ExitCode = nil
restored.ExitMessage = ""
restored.HypervisorType = targetHypervisor
- restored.MachineType, err = normalizeSnapshotMachineType(restored.MachineType, rec.StoredMetadata.HypervisorType, targetHypervisor)
- if err != nil {
- return nil, err
- }
+ restored.MachineType = targetMachineType
starter, err := m.getVMStarter(targetHypervisor)
if err != nil {
@@ -400,6 +401,10 @@
if err != nil {
return nil, err
}
+ targetMachineType, err := m.resolveSnapshotTargetMachineType(rec.StoredMetadata, targetHypervisor)
+ if err != nil {
+ return nil, err
+ }
forkID := cuid2.Generate()
if _, err := m.loadMetadata(forkID); err == nil {
@@ -441,10 +446,7 @@
forkMeta.HypervisorPID = nil
forkMeta.DataDir = dstDir
forkMeta.HypervisorType = targetHypervisor
- forkMeta.MachineType, err = normalizeSnapshotMachineType(forkMeta.MachineType, rec.StoredMetadata.HypervisorType, targetHypervisor)
- if err != nil {
- return nil, err
- }
+ forkMeta.MachineType = targetMachineType
if targetHypervisor != rec.StoredMetadata.HypervisorType {
hvVersion, err := starter.GetVersion(m.paths)
if err != nil {
@@ -556,6 +558,30 @@
return requested, nil
}
+func (m *manager) resolveSnapshotTargetMachineType(stored StoredMetadata, target hypervisor.Type) (hypervisor.MachineType, error) {
+ if target == hypervisor.TypeQEMUMicroVM {
+ var gpu *GPUConfig
+ if stored.GPUProfile != "" {
+ gpu = &GPUConfig{Profile: stored.GPUProfile}
+ }
+ if _, err := m.resolveCreateMachineType(CreateInstanceRequest{
+ HotplugSize: stored.HotplugSize,
+ Devices: stored.Devices,
+ Volumes: stored.Volumes,
+ NetworkEnabled: stored.NetworkEnabled,
+ GPU: gpu,
+ }, target); err != nil {
+ return "", err
+ }
+ }
+
+ machineType, err := normalizeSnapshotMachineType(stored.MachineType, stored.HypervisorType, target)
+ if err != nil {
+ return "", err
+ }
+ return machineType, nil
+}
+
func resolveSnapshotTargetState(kind SnapshotKind, requested State) (State, error) {
resolved, err := snapshotstore.ResolveTargetState(kind, string(requested))
if err != nil {
diff --git a/lib/instances/snapshot_test.go b/lib/instances/snapshot_test.go
--- a/lib/instances/snapshot_test.go
+++ b/lib/instances/snapshot_test.go
@@ -84,6 +84,74 @@
assert.ErrorIs(t, err, ErrInvalidRequest)
}
+func TestStoppedSnapshotRestoreRejectsIncompatibleMicroVMSwitch(t *testing.T) {
+ t.Parallel()
+ mgr, _ := setupTestManager(t)
+ if _, err := mgr.getVMStarter(hypervisor.TypeQEMUMicroVM); err != nil {
+ t.Skipf("qemu-microvm starter unavailable: %v", err)
+ }
+ ctx := context.Background()
+
+ sourceID := "snapshot-stopped-restore-microvm-src"
+ createStoppedSnapshotSourceFixture(t, mgr, sourceID, "snapshot-stopped-restore-microvm-src", mgr.defaultHypervisor)
+
+ sourceMeta, err := mgr.loadMetadata(sourceID)
+ require.NoError(t, err)
+ sourceMeta.HotplugSize = 1
+ require.NoError(t, mgr.saveMetadata(sourceMeta))
+
+ snap, err := mgr.CreateSnapshot(ctx, sourceID, CreateSnapshotRequest{
+ Kind: SnapshotKindStopped,
+ Name: "stopped-microvm-restore",
+ })
+ require.NoError(t, err)
+
+ _, err = mgr.RestoreSnapshot(ctx, sourceID, snap.Id, RestoreSnapshotRequest{
+ TargetState: StateStopped,
+ TargetHypervisor: hypervisor.TypeQEMUMicroVM,
+ })
+ require.Error(t, err)
+ assert.ErrorIs(t, err, ErrInvalidRequest)
+ assert.Contains(t, err.Error(), "does not support hotplug memory")
+
+ after, err := mgr.loadMetadata(sourceID)
+ require.NoError(t, err)
+ assert.Equal(t, mgr.defaultHypervisor, after.HypervisorType)
+ assert.Equal(t, int64(1), after.HotplugSize)
+}
+
+func TestStoppedSnapshotForkRejectsIncompatibleMicroVMSwitch(t *testing.T) {
+ t.Parallel()
+ mgr, _ := setupTestManager(t)
+ if _, err := mgr.getVMStarter(hypervisor.TypeQEMUMicroVM); err != nil {
+ t.Skipf("qemu-microvm starter unavailable: %v", err)
+ }
+ ctx := context.Background()
+
+ sourceID := "snapshot-stopped-fork-microvm-src"
+ createStoppedSnapshotSourceFixture(t, mgr, sourceID, "snapshot-stopped-fork-microvm-src", mgr.defaultHypervisor)
+
+ sourceMeta, err := mgr.loadMetadata(sourceID)
+ require.NoError(t, err)
+ sourceMeta.HotplugSize = 1
+ require.NoError(t, mgr.saveMetadata(sourceMeta))
+
+ snap, err := mgr.CreateSnapshot(ctx, sourceID, CreateSnapshotRequest{
+ Kind: SnapshotKindStopped,
+ Name: "stopped-microvm-fork",
+ })
+ require.NoError(t, err)
+
+ _, err = mgr.ForkSnapshot(ctx, snap.Id, ForkSnapshotRequest{
+ Name: "snapshot-fork-microvm-invalid",
+ TargetState: StateStopped,
+ TargetHypervisor: hypervisor.TypeQEMUMicroVM,
+ })
+ require.Error(t, err)
+ assert.ErrorIs(t, err, ErrInvalidRequest)
+ assert.Contains(t, err.Error(), "does not support hotplug memory")
+}
+
func TestRestoreSnapshotCancelsSourceInstanceCompressionJob(t *testing.T) {
t.Parallel()You can send follow-ups to the cloud agent here.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.
Autofix Details
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: Pool rejects backend type switch
- GetOrCreateForType now evicts stale pooled clients on hypervisor-type mismatch and reconnects instead of returning a permanent mismatch error.
- ✅ Fixed: Hardcoded microvm type switches
- I replaced direct qemu-microvm conditionals in shared lifecycle paths with a typed hypervisor version-policy map and helper functions.
Or push these changes by commenting:
@cursor push fa369c1f56
Preview (fa369c1f56)
diff --git a/lib/hypervisor/qemu/pool.go b/lib/hypervisor/qemu/pool.go
--- a/lib/hypervisor/qemu/pool.go
+++ b/lib/hypervisor/qemu/pool.go
@@ -1,7 +1,6 @@
package qemu
import (
- "fmt"
"sync"
"github.com/kernel/hypeman/lib/hypervisor"
@@ -28,11 +27,11 @@
// Try read lock first for existing connection
clientPool.RLock()
if client, ok := clientPool.clients[socketPath]; ok {
- clientPool.RUnlock()
- if client.hypervisorType != hypervisorType {
- return nil, poolTypeMismatchError(socketPath, client.hypervisorType, hypervisorType)
+ if client.hypervisorType == hypervisorType {
+ clientPool.RUnlock()
+ return client, nil
}
- return client, nil
+ // Backend identity changed for this socket path. Recreate under write lock.
}
clientPool.RUnlock()
@@ -42,10 +41,11 @@
// Double-check after acquiring write lock
if client, ok := clientPool.clients[socketPath]; ok {
- if client.hypervisorType != hypervisorType {
- return nil, poolTypeMismatchError(socketPath, client.hypervisorType, hypervisorType)
+ if client.hypervisorType == hypervisorType {
+ return client, nil
}
- return client, nil
+ // Stale pooled backend type for this socket path. Drop and reconnect.
+ removeLocked(socketPath)
}
// Create new client
@@ -58,20 +58,22 @@
return client, nil
}
-func poolTypeMismatchError(socketPath string, cached, requested hypervisor.Type) error {
- return fmt.Errorf("QEMU client for %s is pooled as hypervisor %s, not %s", socketPath, cached, requested)
-}
-
// Remove closes and removes a client from the pool.
// Called automatically on errors to allow fresh reconnection.
// Close is done asynchronously to avoid blocking if the connection is in a bad state.
func Remove(socketPath string) {
clientPool.Lock()
defer clientPool.Unlock()
+ removeLocked(socketPath)
+}
+// removeLocked removes an entry while clientPool lock is held.
+func removeLocked(socketPath string) {
if client, ok := clientPool.clients[socketPath]; ok {
delete(clientPool.clients, socketPath)
// Close asynchronously to avoid blocking on stuck connections
- go client.client.Close()
+ if client.client != nil {
+ go client.client.Close()
+ }
}
}
diff --git a/lib/hypervisor/qemu/pool_test.go b/lib/hypervisor/qemu/pool_test.go
--- a/lib/hypervisor/qemu/pool_test.go
+++ b/lib/hypervisor/qemu/pool_test.go
@@ -7,7 +7,7 @@
"github.com/stretchr/testify/require"
)
-func TestGetOrCreateForTypeRejectsCachedBackendMismatch(t *testing.T) {
+func TestGetOrCreateForTypeEvictsCachedBackendMismatch(t *testing.T) {
socketPath := t.TempDir() + "/qemu.sock"
clientPool.Lock()
clientPool.clients[socketPath] = &QEMU{socketPath: socketPath, hypervisorType: hypervisor.TypeQEMU}
@@ -19,5 +19,9 @@
})
_, err := GetOrCreateForType(socketPath, hypervisor.TypeQEMUMicroVM)
- require.ErrorContains(t, err, "pooled as hypervisor qemu, not qemu-microvm")
+ require.Error(t, err)
+ clientPool.RLock()
+ _, stillPooled := clientPool.clients[socketPath]
+ clientPool.RUnlock()
+ require.False(t, stillPooled, "stale cached backend must be evicted after mismatch")
}
diff --git a/lib/instances/create.go b/lib/instances/create.go
--- a/lib/instances/create.go
+++ b/lib/instances/create.go
@@ -748,13 +748,13 @@
return fmt.Errorf("get vm starter: %w", err)
}
- // qemu-microvm snapshots are tied to the binary that boots the VM. Refresh
- // metadata on every cold start so host upgrades do not leave the instance's
- // reported version pinned to its original creation time.
- if stored.HypervisorType == hypervisor.TypeQEMUMicroVM {
+ // Some hypervisors tie snapshot compatibility to the exact runtime binary.
+ // Refresh metadata on cold start so host upgrades do not leave the stored
+ // version pinned to original creation time.
+ if refreshHypervisorVersionOnColdStart(stored.HypervisorType) {
detectedVersion, err := starter.GetVersion(m.paths)
if err != nil {
- return fmt.Errorf("get QEMU version for qemu-microvm start: %w", err)
+ return fmt.Errorf("get hypervisor version for %s start: %w", stored.HypervisorType, err)
}
stored.HypervisorVersion = detectedVersion
}
diff --git a/lib/instances/hypervisor_version.go b/lib/instances/hypervisor_version.go
--- a/lib/instances/hypervisor_version.go
+++ b/lib/instances/hypervisor_version.go
@@ -8,19 +8,46 @@
"github.com/kernel/hypeman/lib/logger"
)
+type hypervisorVersionPolicy struct {
+ enforceExactInstalledVersion bool
+ refreshOnColdStart bool
+}
+
+var hypervisorVersionPoliciesByType = map[hypervisor.Type]hypervisorVersionPolicy{
+ hypervisor.TypeQEMUMicroVM: {
+ enforceExactInstalledVersion: true,
+ refreshOnColdStart: true,
+ },
+}
+
+func resolveHypervisorVersionPolicy(hvType hypervisor.Type) hypervisorVersionPolicy {
+ if policy, ok := hypervisorVersionPoliciesByType[hvType]; ok {
+ return policy
+ }
+ return hypervisorVersionPolicy{}
+}
+
+func enforceExactInstalledHypervisorVersion(hvType hypervisor.Type) bool {
+ return resolveHypervisorVersionPolicy(hvType).enforceExactInstalledVersion
+}
+
+func refreshHypervisorVersionOnColdStart(hvType hypervisor.Type) bool {
+ return resolveHypervisorVersionPolicy(hvType).refreshOnColdStart
+}
+
func (m *manager) resolveCreateHypervisorVersion(
ctx context.Context,
starter hypervisor.VMStarter,
hvType hypervisor.Type,
requested string,
) (string, error) {
- if hvType == hypervisor.TypeQEMUMicroVM {
+ if enforceExactInstalledHypervisorVersion(hvType) {
detected, err := starter.GetVersion(m.paths)
if err != nil {
- return "", fmt.Errorf("get QEMU version for qemu-microvm: %w", err)
+ return "", fmt.Errorf("get installed hypervisor version for %s: %w", hvType, err)
}
if requested != "" && requested != detected {
- return "", fmt.Errorf("%w: requested qemu-microvm version %q does not match installed QEMU %q", ErrInvalidRequest, requested, detected)
+ return "", fmt.Errorf("%w: requested %s hypervisor version %q does not match installed version %q", ErrInvalidRequest, hvType, requested, detected)
}
return detected, nil
}
diff --git a/lib/instances/snapshot.go b/lib/instances/snapshot.go
--- a/lib/instances/snapshot.go
+++ b/lib/instances/snapshot.go
@@ -532,8 +532,8 @@
if target != source.HypervisorType {
version, err = starter.GetVersion(m.paths)
if err != nil {
- if target == hypervisor.TypeQEMUMicroVM {
- return nil, "", fmt.Errorf("get QEMU version for qemu-microvm snapshot target: %w", err)
+ if enforceExactInstalledHypervisorVersion(target) {
+ return nil, "", fmt.Errorf("get installed hypervisor version for %s snapshot target: %w", target, err)
}
logger.FromContext(ctx).WarnContext(ctx, "failed to get hypervisor version", "hypervisor", target, "error", err)
version = "unknown"You can send follow-ups to the cloud agent here.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: Pool close blocks under lock
- Backend-mismatch eviction and resetClient now remove entries and close QMP clients asynchronously so pool lock holders cannot block on stuck Close calls.
- ✅ Fixed: Stale Remove evicts new client
- Pool removal now supports pointer-guarded eviction and all QEMU instance error paths remove only if the pooled entry is the same client instance.
Or push these changes by commenting:
@cursor push a200381e50
Preview (a200381e50)
diff --git a/lib/hypervisor/qemu/pool.go b/lib/hypervisor/qemu/pool.go
--- a/lib/hypervisor/qemu/pool.go
+++ b/lib/hypervisor/qemu/pool.go
@@ -43,7 +43,8 @@
}
delete(clientPool.clients, socketPath)
if client.client != nil {
- _ = client.client.Close()
+ // Match Remove's behavior so a stuck QMP close doesn't block the pool lock.
+ go client.client.Close()
}
}
@@ -57,29 +58,33 @@
return client, nil
}
-// resetClient synchronously drops a pooled connection before a new QEMU
+// resetClient drops a pooled connection before a new QEMU
// process reuses the same socket path.
func resetClient(socketPath string) {
- clientPool.Lock()
- defer clientPool.Unlock()
- if client, ok := clientPool.clients[socketPath]; ok {
- delete(clientPool.clients, socketPath)
- if client.client != nil {
- _ = client.client.Close()
- }
- }
+ removeIfCurrent(socketPath, nil)
}
// Remove closes and removes a client from the pool.
// Called automatically on errors to allow fresh reconnection.
// Close is done asynchronously to avoid blocking if the connection is in a bad state.
func Remove(socketPath string) {
+ removeIfCurrent(socketPath, nil)
+}
+
+// removeIfCurrent removes and closes the pooled client if the current entry
+// matches expected. Passing nil expected removes whatever is currently pooled.
+func removeIfCurrent(socketPath string, expected *QEMU) {
clientPool.Lock()
defer clientPool.Unlock()
if client, ok := clientPool.clients[socketPath]; ok {
+ if expected != nil && client != expected {
+ return
+ }
delete(clientPool.clients, socketPath)
- // Close asynchronously to avoid blocking on stuck connections
- go client.client.Close()
+ // Close asynchronously to avoid blocking on stuck connections.
+ if client.client != nil {
+ go client.client.Close()
+ }
}
}
diff --git a/lib/hypervisor/qemu/pool_test.go b/lib/hypervisor/qemu/pool_test.go
--- a/lib/hypervisor/qemu/pool_test.go
+++ b/lib/hypervisor/qemu/pool_test.go
@@ -26,3 +26,42 @@
clientPool.RUnlock()
require.False(t, stillCached, "stale backend client must be removed before reconnect")
}
+
+func TestRemoveIfCurrentSkipsReplacementClient(t *testing.T) {
+ socketPath := t.TempDir() + "/qemu.sock"
+ stale := &QEMU{socketPath: socketPath, hypervisorType: hypervisor.TypeQEMU}
+ replacement := &QEMU{socketPath: socketPath, hypervisorType: hypervisor.TypeQEMUMicroVM}
+
+ clientPool.Lock()
+ clientPool.clients[socketPath] = replacement
+ clientPool.Unlock()
+ t.Cleanup(func() {
+ clientPool.Lock()
+ delete(clientPool.clients, socketPath)
+ clientPool.Unlock()
+ })
+
+ removeIfCurrent(socketPath, stale)
+
+ clientPool.RLock()
+ cached, ok := clientPool.clients[socketPath]
+ clientPool.RUnlock()
+ require.True(t, ok, "replacement client should remain pooled")
+ require.Same(t, replacement, cached, "stale remove must not evict replacement client")
+}
+
+func TestRemoveIfCurrentRemovesMatchingClient(t *testing.T) {
+ socketPath := t.TempDir() + "/qemu.sock"
+ client := &QEMU{socketPath: socketPath, hypervisorType: hypervisor.TypeQEMU}
+
+ clientPool.Lock()
+ clientPool.clients[socketPath] = client
+ clientPool.Unlock()
+
+ removeIfCurrent(socketPath, client)
+
+ clientPool.RLock()
+ _, ok := clientPool.clients[socketPath]
+ clientPool.RUnlock()
+ require.False(t, ok, "matching pooled client should be removed")
+}
diff --git a/lib/hypervisor/qemu/qemu.go b/lib/hypervisor/qemu/qemu.go
--- a/lib/hypervisor/qemu/qemu.go
+++ b/lib/hypervisor/qemu/qemu.go
@@ -71,7 +71,7 @@
// This sends a graceful shutdown signal to the guest.
func (q *QEMU) DeleteVM(ctx context.Context) error {
if err := q.client.SystemPowerdown(); err != nil {
- Remove(q.socketPath)
+ removeIfCurrent(q.socketPath, q)
return err
}
clearBalloonTargetCache(q.socketPath)
@@ -81,11 +81,11 @@
// Shutdown stops the QEMU process.
func (q *QEMU) Shutdown(ctx context.Context) error {
if err := q.client.Quit(); err != nil {
- Remove(q.socketPath)
+ removeIfCurrent(q.socketPath, q)
return err
}
// Connection is gone after quit, remove from pool
- Remove(q.socketPath)
+ removeIfCurrent(q.socketPath, q)
clearBalloonTargetCache(q.socketPath)
return nil
}
@@ -94,7 +94,7 @@
func (q *QEMU) GetVMInfo(ctx context.Context) (*hypervisor.VMInfo, error) {
status, err := q.client.Status()
if err != nil {
- Remove(q.socketPath)
+ removeIfCurrent(q.socketPath, q)
return nil, fmt.Errorf("query status: %w", err)
}
@@ -129,7 +129,7 @@
// Pause suspends VM execution.
func (q *QEMU) Pause(ctx context.Context) error {
if err := q.client.Stop(); err != nil {
- Remove(q.socketPath)
+ removeIfCurrent(q.socketPath, q)
return err
}
return nil
@@ -138,7 +138,7 @@
// Resume continues VM execution.
func (q *QEMU) Resume(ctx context.Context) error {
if err := q.client.Continue(); err != nil {
- Remove(q.socketPath)
+ removeIfCurrent(q.socketPath, q)
return err
}
return nil
@@ -153,13 +153,13 @@
memoryFile := destPath + "/memory"
uri := "exec:cat > " + memoryFile
if err := q.client.Migrate(uri); err != nil {
- Remove(q.socketPath)
+ removeIfCurrent(q.socketPath, q)
return fmt.Errorf("migrate: %w", err)
}
// Wait for migration to complete
if err := q.client.WaitMigration(ctx, migrationTimeout); err != nil {
- Remove(q.socketPath)
+ removeIfCurrent(q.socketPath, q)
return fmt.Errorf("wait migration: %w", err)
}
@@ -197,7 +197,7 @@
return fmt.Errorf("target guest memory %d must be non-negative", bytes)
}
if err := q.client.Balloon(bytes); err != nil {
- Remove(q.socketPath)
+ removeIfCurrent(q.socketPath, q)
return fmt.Errorf("set balloon target: %w", err)
}
balloonTargetCache.Store(q.socketPath, bytes)You can send follow-ups to the cloud agent here.
Reviewed by Cursor Bugbot for commit 33cffb1. Configure here.


Summary
qemu-microvmas a first-class hypervisor backend on Linux amd64microvmboard with virtio-mmio disks, networking, vsock, and balloon devicesqemubehavior unchanged (q35on amd64,virton arm64)The public API exposes only
hypervisor: qemu-microvm; QEMU machine types remain an internal restore detail. This avoids adding a QEMU-specificmachine_typefield to the generic instance API.Background
QEMU's
microvmmachine type is a minimalist x86 virtual platform inspired by Firecracker. Compared with the general-purposeq35PC model, it deliberately omits PCI and ACPI, exposes paravirtualized devices overvirtio-mmio, and supports at most eight of those devices. QEMU designed it for short-lived guests and as a baseline for optimizing boot time and footprint.That model fits Hypeman's direct-kernel Linux guests: we already control the kernel/initrd and primarily need virtio disks, networking, vsock, and ballooning. Selecting
qemu-microvmavoids initializing hardware that these workloads do not use while retaining Hypeman's existing QEMU/QMP lifecycle, snapshot, and fork machinery.It is not a transparent replacement for standard QEMU. Upstream documents no PCI-only devices, no hotplug, and no live migration across QEMU versions. This PR therefore exposes it as a distinct Hypeman hypervisor capability contract instead of a generic QEMU board toggle, and validates those constraints before creating a VM.
Further reading:
microvmvirtual platform — authoritative device model, options, and limitationspc/q35machine familiesValidation
go build ./...go vet ./lib/hypervisor/... ./lib/instances/... ./cmd/api/api/...go test ./lib/hypervisor/qemu ./cmd/api/api -count=1make test TEST='TestQEMUMicroVM.*' TEST_TIMEOUT=12mTestQEMUBasicEndToEndTestQEMUStandbyAndRestoreTestQEMUForkFromRunningNetworkqemu-microvmbooted Alpine, reachedRunning, and returnedx86_64; standardqemucontinued to use q35machine_typeremainsPerformance sample
Three sequential nginx samples on this shared Linux amd64/KVM host:
These numbers are non-gating; the included benchmark is opt-in.
Closes #275
Note
Medium Risk
Touches VM lifecycle, snapshot restore, and QMP pooling; microvm’s exact-QEMU-version standby contract can surprise operators after upgrades, though constraints are validated early and covered by integration tests.
Overview
Adds
qemu-microvmas a selectable hypervisor alongside standardqemu, wired through the API/CLI/OpenAPI and a dedicated QEMU starter that pins QEMU’smicrovmboard with virtio-mmio devices (no PCI transport).QEMU layer:
VMConfig.MachineTypedrives board selection; microvm builds use-no-user-config, reject hotplug/PCI, and enforce the eight virtio-mmio device budget.qemu-config.jsonnow storesQEMUVersionfor microvm; standby/warm restore refuses mismatched QEMU versions (RequiresExactSnapshotVersion). The QMP client pool is backend-aware and avoids evicting a replacement client on stale errors.Instances layer: Create and stopped-snapshot restore/fork validate microvm limits (volumes, GPU/PCI, hotplug) before mutations; version-locked backends resolve/detect QEMU version on create and cold start. Stopped snapshots may
target_hypervisor: qemu-microvm(and switch from standard qemu).Docs add QEMU dev deps and usage; an opt-in boot/RSS benchmark compares q35 vs microvm.
Reviewed by Cursor Bugbot for commit fc1fb3e. Bugbot is set up for automated code reviews on this repo. Configure here.