VM Lifecycle Management in KubeVirt#
KubeVirt manages VM lifecycle through two parallel state layers:
- VMIPhase — the Kubernetes-API-facing phase of a
VirtualMachineInstance(Pending → Running → Succeeded/Failed) - Domain LifeCycle — the libvirt/QEMU domain state internal to virt-launcher (Running, Paused, Shutoff, PMSuspended, etc.)
Four components coordinate transitions:
| Component | Role |
|---|---|
| virt-api | Accepts user lifecycle requests (start/stop/pause/restart), validates current state, mutates the VM spec or StateChangeRequests queue |
| virt-controller | Watches VirtualMachine objects; drives the RunStrategy policy by creating/deleting VMI objects |
| virt-handler | Per-node daemon; reconciles live domain state with the desired VMI spec; maps domain states to VMI phases |
| virt-launcher | Wraps the QEMU process; streams libvirt domain events back to virt-handler via gRPC |
VMI Phases (Kubernetes-Facing)#
VirtualMachineInstancePhase is the user-visible lifecycle state, defined in staging/src/kubevirt.io/api/core/v1/types.go:
| Phase | Meaning |
|---|---|
Pending | Accepted by the system; pod not yet created |
Scheduling | Launcher pod created but not yet scheduled/running |
Scheduled | Pod running; virt-handler has taken over |
Running | VM is executing (also set while domain is Paused or PMSuspended) |
WaitingForSync | VMI is a migration target waiting for sync |
Succeeded | VM stopped voluntarily |
Failed | VM crashed or disappeared unexpectedly |
Unknown | State could not be obtained |
IsFinal() returns true for Succeeded and Failed — the terminal states that trigger virt-controller restart/cleanup logic. The VirtualMachineStatus.PrintableStatus field provides a finer-grained human-readable status beyond the raw phase .
Domain LifeCycle States (libvirt-Facing)#
The LifeCycle type in pkg/virt-launcher/virtwrap/api/schema.go mirrors libvirt's domain states:
| LifeCycle | Notes |
|---|---|
NoState | Domain doesn't exist or state is unknown |
Running | VM is executing |
Blocked | VM waiting for I/O |
Paused | VM suspended; reason determines behavior |
ShuttingDown | ACPI shutdown signal received |
Shutoff | VM is stopped |
Crashed | VM panicked |
PMSuspended | Guest-initiated power management suspend |
Unknown | Domain exists but virt-launcher is unreachable (prevents spurious deletions during informer re-lists) |
Every state transition also carries a StateChangeReason . The most operationally significant are:
- Shutoff reasons:
Shutdown(guest-initiated),Destroyed(force-killed),Migrated,Crashed,Panicked,Saved,FromSnapshot - Pause reasons:
User,Migration,IOError(triggers auto-retry),Postcopy,PostcopyFailed(triggers irrecoverable shutdown),Snapshot,StartingUp
DomainStatus stores both the current LifeCycle and StateChangeReason together .
VM RunStrategy (Policy Layer)#
The VirtualMachine object's RunStrategy field controls what the virt-controller does when the VMI reaches a terminal phase :
| RunStrategy | Behavior |
|---|---|
Always | Restart VMI on any terminal phase |
Halted | Never run; stop any running VMI |
Manual | Start/stop only via explicit API requests |
RerunOnFailure | Restart on Failed; do not restart on Succeeded |
Once | Start once; never restart |
WaitAsReceiver | Create VMI in receiver mode for incoming live migration |
The controller implements this in syncRunStrategy(). Lifecycle API calls that don't match the current RunStrategy are rejected — for example, calling /start on a VM with RunStrategyAlways returns a 409 Conflict .
User requests that are compatible queue VirtualMachineStateChangeRequest items (StartRequest / StopRequest) into VM.Status.StateChangeRequests . The virt-controller drains this queue as it reconciles.
Component Responsibilities#
virt-api — pkg/virt-api/rest/lifecycle.go#
Exposes subresource endpoints: start, stop, restart, pause, unpause, softreboot, reset, migrate. Each handler:
- Validates the current VMI phase and RunStrategy compatibility before accepting the request.
- For
start/stop/restart: patchesVM.Spec(forAlways/Once) or appends toVM.Status.StateChangeRequests(forManual/RerunOnFailure) . - For
pause/unpause: forwards the request directly to virt-handler via a PUT to the handler's REST endpoint — no VM-level state is mutated .
Key validation rules :
startis rejected if the VMI is already in a non-final phase.pauserequires VMI phaseRunningand theVirtualMachineInstancePausedcondition absent.unpauserequires theVirtualMachineInstancePausedcondition present; also blocked if a snapshot is in progress.
virt-controller — pkg/virt-controller/watch/vm/vm.go#
The controller watches VirtualMachine and VirtualMachineInstance objects. Its entry point is Execute(), which calls sync() → syncRunStrategy(). For each RunStrategy branch it either creates a new VMI, deletes the existing one, or does nothing. Start failure crash-loop backoff is tracked in VirtualMachineStatus.StartFailure .
virt-handler — pkg/virt-handler/vm.go#
The per-node reconciliation loop runs sync() , which resolves four decision flags:
shouldShutdown— domain alive + VMI deleted or deletion timestamp set → callprocessVmShutdown()(ACPI signal)shouldDelete— domain not alive + VMI in final state → calldeleteVM()(cleanup)shouldUpdate— VMI active and phase in sync → callprocessVmUpdate()(sync spec, hotplug)forceShutdownIrrecoverable— domain paused due toPostcopyFailed→ callprocessVmDestroy()(force kill)
After each action, virt-handler calls calculateVmPhaseForStatusReason() to translate the current domain LifeCycle + StateChangeReason into a VirtualMachineInstancePhase and writes it back to VMI.Status.
virt-launcher — pkg/virt-launcher/notify-client/client.go#
StartDomainNotifier() registers libvirt event callbacks (lifecycle, device add/remove, job completed). On each event it:
- Fetches fresh domain state from libvirt.
- Calls
domain.SetState(ConvState(status), ConvReason(status, reason))to map libvirt enums to KubeVirt types. - Marshals the domain object to JSON and sends it to virt-handler via gRPC (
SendDomainEvent()), retrying on transient errors (Unavailable,DeadlineExceeded,Aborted).
Key State Mappings#
The following table summarizes how calculateVmPhaseForStatusReason() translates domain state → VMI phase:
| Domain LifeCycle | Reason | VMI Phase | Notes |
|---|---|---|---|
Running, Blocked, PMSuspended | any | Running | PMSuspended maps to Running, not a distinct phase |
Paused | any except PostcopyFailed | Running | VirtualMachineInstancePaused condition is set separately |
Paused | PostcopyFailed | Failed | Triggers forceShutdownIrrecoverable |
Shutoff / Crashed | Crashed, Panicked, Destroyed (ACPI) | Failed | |
Shutoff | Shutdown, Saved, FromSnapshot | Succeeded | |
Shutoff | Migrated | (current phase unchanged) | State is indeterminate post-migration |
No domain, VMI Scheduled | launcher responsive | Scheduled | Waiting for domain to start |
| No domain, VMI is migration target | — | WaitingForSync | |
No domain, VMI never reached Running | launcher unresponsive | Failed |
Paused vs. Running: The VMI phase stays Running while the guest is paused — the guest-paused state is surfaced via the VirtualMachineInstancePaused condition on the VMI, not via a distinct phase. This means controllers and users need to check conditions, not just phase, to determine if a VM is actually executing.
IOError handling: A domain paused with reason IOError does not immediately map to failure. virt-handler's IO error retry manager delays the shouldUpdate action, giving storage time to recover before escalating .