Module 2: VM Lifecycle Management
This lab module provides a structured introduction to virtual machine (VM) lifecycle management using Red Hat OpenShift Virtualization. Attendees will gain practical skills to create, operate, migrate, snapshot and decommission virtual machines within a Red Hat OpenShift cluster.
Red Hat OpenShift Virtualization is an add-on that enables running virtual machines alongside containers on the same platform. It is built on the open-source KubeVirt project and uses the Linux Kernel-based Virtual Machine (KVM) hypervisor underneath.
As such, VMs are integrated to the platform using Custom Resource and Custom Resource Definition objects. To put it simply, VM are not treated as foreign objects managed by a separate hypervisor but like any containerized application. This means every tool — oc, kubectl, RBAC, NetworkPolicy, GitOps — applies equally to VMs and containers.
By the end of this module, attendees will understand how traditional VM operations map to Red Hat OpenShift native constructs and can manage the VM lifecycle using both the web console and/or command-line tools.
Learning objectives
By the end of this module, you will be able to:
-
What are the core Custom Resources used by Red Hat OpenShift Virtualization
-
Virtual Machine various states and their meaning
Core Red Hat OpenShift Virtualization Custom Resources (CRs)
Three Custom Resource Definitions (CRDs) form the foundation of VM lifecycle management:
VirtualMachine(VM)-
The persistent definition of a virtual machine, analogous to a Deployment. It records the desired state and survives reboots. Created once, it persists until explicitly deleted.
VirtualMachineInstance(VMI)-
Represents a single running instance of a VM, analogous to a Pod. It is ephemeral: a VMI is created at start-up and deleted at shutdown. The VM controller reconciles the desired state in the VM object against the actual VMI.
DataVolume(DV)-
A CDI (Containerized Data Importer) object that manages the import, cloning, or upload of disk images into PersistentVolumeClaims. DVs handle the heavy lifting of getting OS images onto cluster storage.
| The VM is to VMI as Deployment is to Pod. The VM is defined once and the controller takes care of the creation, the restart, and the deletion of the corresponding VMI objects. |
Virtual Machine states
A virtual machine transitions through specific states during its lifecycle. It is essential to understand each state:
| State | Description | Allowed Transitions |
|---|---|---|
Stopped |
VM is defined but not running. No CPU or memory consumed but storage is allocated. |
→ Running, → Paused (offline) |
Starting |
VMI pod is being scheduled and then launched. |
→ Running, → Failed |
Running |
VM is active. A VMI object and pod exist on a node for this VM. |
→ Paused, → Stopped, → Migrating |
Paused |
vCPU execution halted; memory retained on the node. Storage remains allocated |
→ Running, → Stopped |
Migrating |
Live migration is in progress to move the VM from one node to another. |
→ Running (on target) |
Failed |
VMI encountered an unrecoverable error. |
→ Stopped (manual restart) |
Creating VMs
Red Hat OpenShift Virtualization provides three primary ways to create a virtual machine, each suited to different workflows:
- Web Console Wizard
-
Guided UI experience with templates. Ideal for ad-hoc creation or new users.
- YAML Manifest
-
Declarative, version-controllable definition. Preferred for automation and GitOps pipelines.
- virtctl CLI
-
Imperative command-line creation. Useful for quick provisioning in scripted environments.
VM manifest anatomy
A VirtualMachine manifest is a standard Kubernetes object. The following annotated example creates a RHEL 9 VM with 2 vCPUs, 4 GiB RAM, and a 30 GiB boot disk cloned from a golden image.
apiVersion: kubevirt.io/v1
kind: VirtualMachine
metadata:
name: rhel9-webserver
namespace: vmlab-student
labels:
app: webserver
spec:
running: false (1)
template:
metadata:
labels:
kubevirt.io/vm: rhel9-webserver
spec:
domain:
cpu:
cores: 2 (2)
sockets: 1
threads: 1
memory:
guest: 4Gi
devices:
disks:
- name: rootdisk
disk:
bus: virtio (3)
- name: cloudinitdisk
disk:
bus: virtio
interfaces:
- name: default
masquerade: {} (4)
networks:
- name: default
pod: {}
volumes:
- name: rootdisk
dataVolume:
name: rhel9-webserver-rootdisk
- name: cloudinitdisk
cloudInitNoCloud:
userData: |
#cloud-config
user: cloud-user
password: redhat123
chpasswd: { expire: False }
runcmd:
- dnf install -y httpd
- systemctl enable --now httpd
dataVolumeTemplates: (5)
- metadata:
name: rhel9-webserver-rootdisk
spec:
storage:
accessModes: [ReadWriteMany]
resources:
requests:
storage: 30Gi
storageClassName: ocs-storagecluster-ceph-rbd-virtualization
source:
pvc:
namespace: openshift-virtualization-os-images
name: rhel9-guest-image
| 1 | VM is created in a stopped state. Set to true to start immediately. |
| 2 | vCPU topology: 2 cores × 1 socket × 1 thread = 2 vCPUs total. |
| 3 | virtio is the paravirtualized driver — fastest option for Linux guests. |
| 4 | masquerade provides NAT connectivity via the pod network. |
| 5 | dataVolumeTemplates ties the disk lifecycle to the VM — the PVC is deleted with the VM. |
Using dataVolumeTemplates keeps the disk definition co-located with the VM. When the VM is deleted, the associated DataVolume and PVC are also deleted automatically unless the PVC is first unlinked.
|
Cloud-init customization
Cloud-init runs inside the VM on first boot and configures the guest operating system. OpenShift Virtualization supports two cloud-init data sources:
cloudInitNoCloud-
Data is passed directly in the VMI spec as a volume. No DHCP or metadata server required. Suitable for most lab and production scenarios.
cloudInitConfigDrive-
Compatible with OpenStack-style config drives. Used when migrating workloads from OpenStack environments.
A practical cloud-init configuration covering common provisioning tasks:
#cloud-config
hostname: rhel9-webserver
user: cloud-user
password: redhat123
chpasswd: { expire: False }
ssh_authorized_keys:
- ssh-rsa AAAA...your-public-key
packages:
- httpd
- firewalld
runcmd:
- systemctl enable --now httpd
- firewall-cmd --add-service=http --permanent
- firewall-cmd --reload
write_files:
- path: /var/www/html/index.html
content: |
<h1>Hello from OpenShift Virtualization!</h1>
Exercise 1: Create and Inspect Virtual Machine
In this exercise you will create a RHEL 9 virtual machine from a YAML manifest, verify the underlying Kubernetes objects, and connect to the VM console.
-
Setup your namespace
oc new-project vmlab-studentExpected output:
Now using project "vmlab-student" on server "https://api.ocp.t26dj.sandbox4392.opentlc.com:6443". You can add applications to this project with the 'new-app' command. For example, try: oc new-app rails-postgresql-example to build a new example application in Ruby. Or use kubectl to deploy a simple Kubernetes application: kubectl create deployment hello-node --image=registry.k8s.io/e2e-test-images/agnhost:2.43 -- /agnhost serve-hostname -
Create VM Manifest
cat > rhel9-webserver.yaml << 'EOF' apiVersion: kubevirt.io/v1 kind: VirtualMachine metadata: name: rhel9-webserver namespace: vmlab-student spec: runStrategy: Manual template: spec: domain: cpu: cores: 2 sockets: 1 threads: 1 memory: guest: 4Gi devices: disks: - name: rootdisk disk: bus: virtio - name: cloudinitdisk disk: bus: virtio interfaces: - name: default masquerade: {} networks: - name: default pod: {} volumes: - name: rootdisk dataVolume: name: rhel9-webserver-rootdisk - name: cloudinitdisk cloudInitNoCloud: userData: | #cloud-config user: cloud-user password: redhat123 chpasswd: { expire: False } ssh_authorized_keys: - {PUBKEY} yum_repos: baseos: name: CentOS Stream 9 BaseOS baseurl: https://mirror.stream.centos.org/9-stream/BaseOS/x86_64/os/ enabled: true gpgcheck: false appstream: name: CentOS Stream 9 AppStream baseurl: https://mirror.stream.centos.org/9-stream/AppStream/x86_64/os/ enabled: true gpgcheck: false packages: - httpd - firewalld runcmd: - echo "This is the WEB server before failure." | sudo tee /var/www/html/index.html - systemctl enable --now firewalld - systemctl enable --now httpd - firewall-cmd --permanent --add-service=http - firewall-cmd --reload dataVolumeTemplates: - metadata: name: rhel9-webserver-rootdisk spec: storage: accessModes: [ReadWriteMany] resources: requests: storage: 30Gi storageClassName: ocs-storagecluster-ceph-rbd-virtualization sourceRef: kind: DataSource name: rhel9 namespace: openshift-virtualization-os-images EOF -
Generate SSH key
ssh-keygenUse <CR> for each question -
Verify the publick key has been generated
cat ~/.ssh/id_rsa.pubExpected output:
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC6K3QfkXhrBio5lWExoRgfx+83GWW5MDFw542kAVU+8eo5YdageNRezQetYTglx5B+ex/u8XfGUSui4RjNx1Z0X7Jjnz8XmaTnDqTu3SwdGyAei+h+UOSq9DkhTLc6HaEm3IoJweNxhObULYF3R+O9r2OdttngYIb02F0YomnGuxI8fvzXzI3ORjCxf6+1N3fLGpw6D+HqVEyhekqI0ZtNSNo8k9vapFT+fI0l5dBT3GVnDhtmI9JD++0QPqEB2CagOIu6XbOAfMz+/8R4MUeNE6J/lWU0MwleF6OudN/o1TZY/1jtQLTuVpGVejUwyHoCBG/h0M72EsAUd3upf8wDI1a/oTvdGyURP7mSQwqRRTSnNLbGRuo3W0BJIQW5FnVUzf461WwZ+Z0DIm65mpmpLNfeqf3aBF90OgwBOnJ1fOCfTGYu7Z9sRXMYUVgjpbdSTR1u7wovFrNvrSj8e6nPp5nHpAq5kOjvtYNo+MUQRIrvVBifbgxR2z/ExhxHlFs= lab-user@bastion.t26dj.internal
-
Customize your VM custom resource before we create it using your new SSH key
export THEKEY=$(cat ~/.ssh/id_rsa.pub) sed -i -e "s_{PUBKEY}_${THEKEY}_g" rhel9-webserver.yamlWe are using _as a delimiter for thesedcommand knowing that the generated public key may contain/characters. -
Apply the manifest to create the virtual machine
oc apply -f rhel9-webserver.yamlExpected output:
virtualmachine.kubevirt.io/rhel9-webserver created
-
Verify the VM status
oc get vm -n vmlab-studentExpected output:
NAME AGE STATUS READY rhel9-webserver <age> Stopped False
You can see that the status of the VM is Stoppedand the readiness isFalse -
Install CLI tool on your bastion node
curl -L https://$(oc get route hyperconverged-cluster-cli-download -n openshift-cnv -o jsonpath='{.spec.host}')/amd64/linux/virtctl.tar.gz -o virtctl.tar.gz tar -xzf virtctl.tar.gz chmod +x virtctl && sudo mv virtctl /usr/local/bin/ -
Start the VM and observe object creation
virtctl start rhel9-webserver -n vmlab-studentThis will create the VMI object Expected output:
VM rhel9-webserver was scheduled to start
-
Watch the VM reach Running state
oc get vm rhel9-webserver -n vmlab-student -wExpected output:
NAME AGE STATUS READY rhel9-webserver <age> Running True
Wait for status to be Runningand readiness to beTrue -
Observe the VMI object that was created
oc get vmi -n vmlab-studentExpected output
NAME AGE PHASE IP NODENAME READY rhel9-webserver <age> Running 10.<x.y.z> ip-<nodename>.us-east-2.compute.internal True
-
Find the virt-launcher pod backing this VM
oc get pods -n vmlab-student -l vm.kubevirt.io/name=rhel9-webserverExpected output:
NAME READY STATUS RESTARTS AGE virt-launcher-rhel9-webserver-<xxx> 2/2 Running 0 <age>
-
Describe the VMI for scheduling and resource details
oc describe vmi rhel9-webserver -n vmlab-studentExpected output:
[truncated] Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal SuccessfulCreate 4m22s virtualmachine-controller Created virtual machine pod virt-launcher-rhel9-webserver-<xxx> Normal Created 4m10s virt-handler VirtualMachineInstance defined. Normal Started 4m10s virt-handler VirtualMachineInstance started.
-
Check DataVolume import status
oc get dv -n vmlab-studentExpected output:
NAME PHASE PROGRESS RESTARTS AGE rhel9-webserver-rootdisk Succeeded 100.0% <age>
Note that the import progress field shows 100.0% -
Connect to the VM console
# Attach to the serial console (Ctrl+] to exit) virtctl console rhel9-webserver -n vmlab-studentIt will take some time for the VM to start and the cloud-initstep to complete. Once messages stop flowing on your screen, the VM is ready. -
Log in to the VM
rhel9-webserver login: cloud-user Password: redhat123
Hit <CR> to get to the login prompt. The VM username is cloud-userand the passwordredhat123as illustrated above. -
Verify the VM deployment is complete
sudo cloud-init statusExpected output:
status: done
-
Verify the WEB server status
sudo systemctl status httpdExpected output:
● httpd.service - The Apache HTTP Server Loaded: loaded (/usr/lib/systemd/system/httpd.service; enabled; preset: di> Active: active (running) since Tue 2026-03-24 15:48:57 EDT; 5h 14min ago Docs: man:httpd.service(8) Main PID: 7918 (httpd) Status: "Total requests: 1; Idle/Busy workers 100/0;Requests/sec: 5.3e-05;> Tasks: 177 (limit: 22797) Memory: 14.9M (peak: 15.4M) CPU: 1min 30.890s [truncated] -
Verify the WEB server is operational
curl http://localhostExpected output:
This is the WEB server before failure.
-
Exit your VM
exitHit CTRL+5to disconnect from the VM console
Verify
-
Confirm the VM is running and on what specific node
oc get pods -l vm.kubevirt.io/name=rhel9-webserver -o custom-columns="NAME:.metadata.name,STATUS:.status.phase,NODE:.spec.nodeName"Expected output:
NAME STATUS NODE virt-launcher-rhel9-webserver-<xxx> Running ip-<nodename>.us-east-2.compute.internal
-
Confirm how many PVCs were created
oc get pvc -o custom-columns="NAME:.metadata.name,SIZE:.status.capacity.storage,ACCESS MODES:.spec.accessModes[0],STORAGE CLASS:.spec.storageClassName"Expected output:
NAME SIZE ACCESS MODES STORAGE CLASS rhel9-webserver-rootdisk 30Gi ReadWriteMany ocs-storagecluster-ceph-rbd-virtualization
-
Confirm the IP address assigned to the VM
oc get vmi -o yaml | grep ipAddress:Expected output:
ipAddress: 10.<x>.<y>.<z>
In this exercise you have verified:
-
On which node the VM is running
-
The number of PVCs for this VM is 1
-
The number of DataVolumes for this VM is 1
-
You have verified the IP address assigned to the VM
Operating virtual machines
virtctl CLI
The virtctl binary is the primary command-line interface for VM operations that go beyond basic Kubernetes resource management. It communicates with the virt-api server to execute actions like start, stop, console access, and live migration. Install it directly from the cluster to ensure version compatibility:
curl -L https://$(oc get route hyperconverged-cluster-cli-download -n openshift-cnv -o jsonpath='{.spec.host}')/amd64/linux/virtctl.tar.gz -o virtctl.tar.gz
tar -xzf virtctl.tar.gz
chmod +x virtctl && sudo mv virtctl /usr/local/bin/
virtctl version
Key virtctl commands and their purposes:
| Command | Purpose |
|---|---|
|
Power on a stopped VM |
|
Gracefully power off a running VM |
|
Graceful reboot (stop + start) |
|
Freeze vCPU execution (memory retained) |
|
Resume a paused VM |
|
Initiate live migration to another node |
|
Attach to the VM serial console |
|
Open a VNC session to the VM display |
|
SSH into the VM via API tunnel |
|
Hot-plug a PVC as a disk |
|
Hot-unplug a previously attached disk |
Power operations
The start, stop, and restart operations manage the VM power state. These actions are recorded in the VM status and trigger VMI creation or deletion accordingly.
# Start a stopped VM
virtctl start <vm-name> -n <namespace>
# Graceful shutdown (sends ACPI power-off signal to guest OS)
virtctl stop <vm-name> -n <namespace>
# Force stop (equivalent to pulling the power cable — use with caution)
virtctl stop --force --grace-period=0 <vm-name> -n <namespace>
# Graceful restart (stop + start, preserves VM definition)
virtctl restart <vm-name> -n <namespace>
# Alternatively, use oc patch to toggle running state
virtctl patch vm <vm-name> -n <namespace> \
--type merge -p '{"spec":{"running":true}}'
| A graceful stop sends ACPI signals to the guest OS, allowing it to flush disk buffers and unmount filesystems cleanly. Always prefer graceful stop over force stop to avoid data corruption. |
Pause and Unpause
Pausing a VM suspends vCPU execution while keeping the VM’s memory contents live in RAM on the node. This is useful for temporarily freezing a VM to take a consistent snapshot or to free up CPU cycles on a busy node.
# Pause a running VM
virtctl pause <vm-name> -n <namespace>
# Verify the paused state
oc get vmi <vm-name> -n <namespace> -o jsonpath='{.status.phase}'
# Resume the VM
virtctl unpause <vm-name> -n <namespace>
Accessing a VM
Red Hat OpenShift Virtualization provides three access methods depending on the use case.
Serial Console
Direct serial console access works regardless of network configuration. Use it for initial setup, troubleshooting boot issues, or when SSH is unavailable.
virtctl console <vm-name> -n <namespace>
# Press Ctrl+] to detach from the console
VNC Graphical Session
VNC provides a graphical display useful for desktop VMs or when a GUI is required.
# Opens VNC in the default browser via port-forward tunnel
virtctl vnc <vm-name> -n <namespace>
SSH via API Tunnel
The virtctl ssh subcommand tunnels SSH through the Kubernetes API, avoiding the need to expose SSH ports via a Service or route.
# SSH using cloud-user and the injected key
virtctl ssh cloud-user@vm/<vm-name> -n <namespace>
# Use a specific private key
virtctl ssh cloud-user@vm/<vm-name> -n <namespace> \
--local-ssh-opts='-i ~/.ssh/id_rsa'
You can also use the VMI to ssh into the virtual machine
|
Storage concepts
Each VM disk maps to a PersistentVolumeClaim (PVC) in Kubernetes. Understanding access modes is critical because they directly affect which operations are possible:
- ReadWriteOnce (RWO)
-
The PVC can be mounted by a single node. Sufficient for stopped or non-migrating VMs, but live migration requires RWX.
- ReadWriteMany (RWX)
-
The PVC can be mounted by multiple nodes simultaneously. Required for live migration because both source and target nodes must access the disk at the same time.
- ReadOnlyMany (ROX)
-
Read-only access from multiple nodes. Used for shared, immutable data.
Always provision VM boot disks with ReadWriteMany access mode if you plan to use live migration. Attempting to migrate a VM with RWO disks will fail with an error in the VirtualMachineInstanceMigration status.
|
DataVolumes and import sources
A DataVolume (DV) orchestrates getting disk images into PVCs. It supports several import sources:
-
PVC clone — clone an existing PVC (most common; used with golden images)
-
HTTP/HTTPS URL — download an image from an external URL
-
Registry — pull a container disk image from a container registry
-
Upload — upload a local disk image via
virtctl image-upload -
Blank — create an empty disk (used for data disks)
Hot-plugging disks
Red Hat OpenShift Virtualization supports attaching and detaching PVC-backed disks to a running VM without rebooting. This is useful for attaching scratch space, shared data volumes, or importing data.
# Create a 10 GiB PVC for the data disk
cat > data-disk.yaml << 'EOF'
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: <pvc-name>
namespace: <namespace>
spec:
accessModes: [ReadWriteOnce]
resources:
requests:
storage: 10Gi
storageClassName: ocs-storagecluster-ceph-rbd-virtualization
EOF
oc apply -f data-disk.yaml
# Wait for the PVC to be Bound
oc get pvc <pvc-name> -n <namespace> -w
# Hot-plug the PVC to the running VM
virtctl addvolume <vm-name> \
--volume-name=<volume-name> \
--claim-name=<pvc-name> \
--persist \
-n <namespace>
# Verify the disk appears inside the guest
virtctl ssh cloud-user@<vm-name> -n <namespace> -- lsblk
# Remove the disk without rebooting
virtctl removevolume <vm-name> \
--volume-name=<volume-name> \
-n <namespace>
Exercise 2: Power Operations and Console Access
-
Confirm the VM is running
oc get vm,vmi -n vmlab-studentExpected output:
NAME AGE STATUS READY virtualmachine.kubevirt.io/rhel9-webserver <age> Running True NAME AGE PHASE IP NODENAME READY virtualmachineinstance.kubevirt.io/rhel9-webserver <age> Running 10.<x>.<y>.<z> ip-<nodename>.us-east-2.compute.internal True
-
Pause the VM
virtctl pause vm rhel9-webserver -n vmlab-studentExpected output:
VMI rhel9-webserver was scheduled to pause
-
Verify the status of the VM
oc get vmi rhel9-webserver -n vmlab-student -o wideExpected output:
NAME AGE PHASE IP NODENAME READY LIVE-MIGRATABLE PAUSED rhel9-webserver <age> Running 10.<x>.<y>.<z> ip-<nodename>.us-east-2.compute.internal False True True
Pay attention to the READYandPAUSEDcolumns for the VMI -
Unpause the VM
virtctl unpause vm rhel9-webserver -n vmlab-studentExpected output:
VMI rhel9-webserver was scheduled to unpause
-
Check the status of the VMI
oc get vmi rhel9-webserver -n vmlab-student -o wideExpected output:
NAME AGE PHASE IP NODENAME READY LIVE-MIGRATABLE PAUSED rhel9-webserver <age> Running 10.<x>.<y>.<z> ip-<nodename>.us-east-2.compute.internal True True
Pay attention to the READYandPAUSEDcolumns for the VMI -
Perform a graceful restart of the VM
virtctl restart rhel9-webserver -n vmlab-studentExpected output:
VM rhel9-webserver was scheduled to restart
-
Watch the VMI object recreate (old one deleted, new one appears)
oc get vmi -n vmlab-student -wExpected output:
NAME AGE PHASE IP NODENAME READY rhel9-webserver <age> Scheduling False rhel9-webserver <age> Running 10.<x>.<y>.<z> ip-<nodename>.us-east-2.compute.internal True
-
SSH through the API tunnel
virtctl ssh -i ~/.ssh/id_rsa cloud-user@vm/rhel9-webserver -n vmlab-studentExpected output:
Register this system with Red Hat Insights: rhc connect Example: # rhc connect --activation-key <key> --organization <org> The rhc client and Red Hat Insights will enable analytics and additional management capabilities on your system. View your connected systems at https://console.redhat.com/insights You can learn more about how to register your system using rhc at https://red.ht/registration Last login: Tue Mar 24 21:46:59 2026
When done exit your ssh session using exitorCTRL+d.Remember you injected your SSH key in the VM custom resource in the first exercise
Verify
Confirm the VMI Custom Resource is linked to the VM state
oc get vmi -n vmlab-student
virtctl stop --force --grace-period=0 rhel9-webserver -n vmlab-student
oc get vmi -n vmlab-student -w
Expected output:
NAME AGE PHASE IP NODENAME READY rhel9-webserver 32m Running 10.131.0.214 ip-10-0-7-182.us-east-2.compute.internal True VM rhel9-webserver was scheduled to stop NAME AGE PHASE IP NODENAME READY rhel9-webserver 32m Running 10.131.0.214 ip-10-0-7-182.us-east-2.compute.internal False rhel9-webserver 32m Succeeded ip-10-0-7-182.us-east-2.compute.internal False rhel9-webserver 32m Succeeded ip-10-0-7-182.us-east-2.compute.internal False rhel9-webserver 32m Succeeded ip-10-0-7-182.us-east-2.compute.internal False
Confirm status of virt-launcher pod
oc get pod -n vmlab-student | grep virt-launcher
Expected output:
No resources found in vmlab-student namespace.
Confirm what happens when the VM restarts
virtctl start rhel9-webserver -n vmlab-student
oc get vmi -n vmlab-student -w
Expected output:
VM rhel9-webserver was scheduled to start NAME AGE PHASE IP NODENAME READY rhel9-webserver 0s Scheduling False rhel9-webserver 12s Scheduled ip-10-0-7-182.us-east-2.compute.internal False rhel9-webserver 13s Scheduled ip-10-0-7-182.us-east-2.compute.internal False rhel9-webserver 13s Running 10.131.0.216 ip-10-0-7-182.us-east-2.compute.internal False rhel9-webserver 13s Running 10.131.0.216 ip-10-0-7-182.us-east-2.compute.internal True rhel9-webserver 13s Running 10.131.0.216 ip-10-0-7-182.us-east-2.compute.internal True
Expected output should show the following
-
The status of the
virt-launcherwhen VM is stopped -
A new VMI object is created upon a restart
Snapshots
VM snapshots
A VirtualMachineSnapshot captures the state of a VM’s disks at a point in time. Snapshots are useful for creating restore points before risky changes such as OS upgrades, configuration changes, or application updates.
Red Hat OpenShift Virtualization snapshots leverage the CSI (Container Storage Interface) VolumeSnapshot API and require a CSI driver that supports snapshots (such as OpenShift Data Foundation / Ceph RBD).
# Stop the VM for a consistent snapshot (preferred)
virtctl stop <vm-name> -n <namespace>
cat << 'EOF' | oc apply -f -
apiVersion: snapshot.kubevirt.io/v1beta1
kind: VirtualMachineSnapshot
metadata:
name: <snapshot-name>
namespace: <namespace>
spec:
source:
apiGroup: kubevirt.io
kind: VirtualMachine
name: <vm-name>
EOF
# Monitor snapshot creation
oc get vmsnapshot -n <namespace> -w
# Verify snapshot is Ready
oc get vmsnapshot <snapshot-name> -n <namespace> \
-o jsonpath='{.status.readyToUse}'
Restoring from a Snapshot
A VirtualMachineRestore reverts a VM to the state captured in a snapshot. The VM must be stopped before initiating a restore.
# Restore the VM to the snapshot (VM must be stopped)
cat << 'EOF' | oc apply -f -
apiVersion: snapshot.kubevirt.io/v1beta1
kind: VirtualMachineRestore
metadata:
name: <restore-name>
namespace: <namespace>
spec:
target:
apiGroup: kubevirt.io
kind: VirtualMachine
name: <vm-name>
virtualMachineSnapshotName: <snapshot-name>
EOF
# Watch restore progress
oc get vmrestore -n <namespace> -w
# Start the VM after restore completes
virtctl start <vm-name> -n <namespace>
Exercise 3: Snapshot, Corrupt Data, and Restore
-
Stop the virtual machine
virtctl stop rhel9-webserver -n vmlab-studentExpected output:
VM rhel9-webserver was scheduled to stop
-
Snapshot the virtual machine
cat << 'EOF' | oc apply -f - apiVersion: snapshot.kubevirt.io/v1beta1 kind: VirtualMachineSnapshot metadata: name: rhel9-webserver-pre-change namespace: vmlab-student spec: source: apiGroup: kubevirt.io kind: VirtualMachine name: rhel9-webserver EOFExpected output:
virtualmachinesnapshot.snapshot.kubevirt.io/rhel9-webserver-pre-change created
-
Verify the snapshot exists and succeeded
oc get vmsnapshot -n vmlab-student -wExpected output:
NAME SOURCEKIND SOURCENAME PHASE READYTOUSE CREATIONTIME ERROR rhel9-webserver-pre-change VirtualMachine rhel9-webserver Succeeded true 5s
Verify the snapshot PHASE=SucceededandREADYTOUSE=true. -
Restart the virtual machine
virtctl start rhel9-webserver -n vmlab-studentExpected output:
VM rhel9-webserver was scheduled to start
-
Simulate failure
virtctl ssh -i ~/.ssh/id_rsa cloud-user@vm/rhel9-webserver -n vmlab-student sudo systemctl stop httpd sudo dnf remove -y httpd -
Verify Webserver stopped inside the VM
curl http://localhost curl: (7) Failed to connect to localhost port 80: Connection refusedDisconnect from the virtual machine using exitfollowed byCTRL+5 -
Stop the virtual machine
virtctl stop rhel9-webserver -n vmlab-studentExpected output:
VM rhel9-webserver was scheduled to stop
-
Restore the virtual machine
cat << 'EOF' | oc apply -f - apiVersion: snapshot.kubevirt.io/v1beta1 kind: VirtualMachineRestore metadata: name: rhel9-webserver-restore-pre namespace: vmlab-student spec: target: apiGroup: kubevirt.io kind: VirtualMachine name: rhel9-webserver virtualMachineSnapshotName: rhel9-webserver-pre-change EOFExpected output:
virtualmachinerestore.snapshot.kubevirt.io/rhel9-webserver-restore-pre created
-
Verify the status of the restore
oc get vmrestore rhel9-webserver-restore-pre -n vmlab-student -wExpected output:
NAME TARGETKIND TARGETNAME COMPLETE RESTORETIME rhel9-webserver-restore-pre VirtualMachine rhel9-webserver true 10s
Verify the status of the restore shows COMPLETE=true -
Restart the virtual machine when restore is complete
virtctl start rhel9-webserver -n vmlab-studentExpected output:
VM rhel9-webserver was scheduled to start
Verify
Now that the restore has completed it is important to verify the restore was successful and the webserver is operational.
Verify the webserver is opertational again
virtctl ssh -i ~/.ssh/id_rsa cloud-user@vm/rhel9-webserver -n vmlab-student \
-c 'curl http://localhost' 2>/dev/null
Expected output:
This is the WEB server before failure.
Expected output should show the following
-
The webserver is operational
VM decommissioning and cleanup
Planned decommissioning
Decommissioning a VM is a multi-step process. Rushing it can result in data loss or orphaned storage objects. Follow this procedure for a safe, clean decommission:
-
Back up any data that needs to be retained (application data, snapshots, exports).
-
Notify users or dependent services of the pending shutdown.
-
Stop the VM gracefully using
virtctl stop. -
Delete any snapshots associated with the VM (optional — frees storage).
-
Delete the VM object — this cascades to delete associated DataVolumes and PVCs created via
dataVolumeTemplates. -
Verify all associated objects are removed.
Deletion commands
# Step 1: Graceful stop
virtctl stop <vm-name> -n <namespace>
# Step 2: Delete snapshots (if any)
oc delete vmsnapshot -n <namespace> --all
# Step 3: Delete the VM (cascades to DVs and PVCs from dataVolumeTemplates)
oc delete vm <vm-name> -n <namespace>
# Step 4: Verify cleanup
oc get vm,vmi,dv,pvc,vmsnapshot -n <namespace>
# Note: Manually created PVCs (not in dataVolumeTemplates) are NOT
# automatically deleted. Remove them explicitly if no longer needed.
oc delete pvc <volume-name> -n <namespace>
Protecting against accidental deletion
To prevent accidental deletion of production VMs, use Kubernetes finalizers or resource annotations. Red Hat OpenShift Virtualization also supports a deletion protection annotation:
# Add deletion protection annotation
oc annotate vm <vm-name> -n <namespace> \
kubevirt.io/vm-deletion-protection=true
# The VM cannot be deleted until the annotation is removed
oc delete vm <vm-name> -n <namespace>
# Error: admission webhook denied the request
# Remove protection before decommissioning
oc annotate vm <vm-name> -n <namespace> \
kubevirt.io/vm-deletion-protection-
VM lifecycle issues
VM stuck in scheduling state
If a VM stays in Scheduling/Pending for more than a few minutes, investigate using the following approach:
# Check VMI events for scheduling errors
oc describe vmi <vm-name> -n <namespace> | grep -A20 Events
# Check the virt-launcher pod (may reveal image pull or resource errors)
oc get pods -n <namespace> -l kubevirt.io/vm=<vm-name>
oc describe pod <virt-launcher-pod> -n <namespace>
# Check node capacity
oc describe nodes | grep -A10 'Allocated resources'
# Check if DataVolume import is still in progress
oc get dv -n <namespace>
oc describe dv <root-disk-name> -n <namespace> | grep -A10 Conditions
Learning outcomes
By completing this module, you should now understand:
-
How to create virtual machines
-
How to alter the state of virtual machines
-
How to connect to virtual machines
-
How to snapshot virtual machines as a user
-
How to restore virtual machines as a user
Module summary
You have successfully explored the OpenShift Virtualization lifecycle management.
What you accomplished:
-
Navigate and identify Red Hat OpenShift Virtualization CRs
-
Interact with the state of a virtual machine
-
Connect to your virtual machine
-
Backed up and restored a virtual machine using snapshots
Key takeaways:
-
You are now familiar with
VM,VMIandDVCustom Resources -
You understand virtual machine states
-
You can start, stop, pause virtual machine
-
You can connect into virtual machines with different methods
-
You are familiar with
virtctl -
You can snapshot and restore virtual machines
Next steps:
Module 3 will cover VM live migration.