Episode 2 of the InventiveHQ KVM virtualization series. Complete the host-installation lesson first so this guest has a validated place to run.
Episode 1 left us with a validated Ubuntu KVM host and an active private libvirt network. This episode gives that host its first guest: an Ubuntu 24.04 LTS virtual machine that can be reached over SSH, inspected through the QEMU guest agent, and recovered through a serial console.
The finished VM starts with 2 vCPUs and 2 GiB RAM, with controlled headroom up to 4 vCPUs and 4 GiB for the resource-management exercise in Episode 3.
Lab verified July 2026 · Ubuntu 26.04 KVM host · Ubuntu 24.04 LTS guest · libvirt 12.0.0 · QEMU 10.2.1
Once the guest is running, use the command builder to inspect it or construct the lifecycle and resource-change sequence used in Episode 3.
What this episode creates
| Resource | Value |
|---|---|
| Domain | demo-ubuntu01 |
| Guest OS | Ubuntu 24.04 LTS cloud image |
| Disk | 24 GiB qcow2 overlay |
| CPU | 2 active, 4 maximum vCPUs |
| Memory | 2 GiB active, 4 GiB maximum |
| Network | libvirt default private NAT network |
| Access | SSH public key, QEMU guest agent, serial console |
| Autostart | Enabled after validation |
The base image, overlay, seed ISO, and domain are separate resources. Keep the base image in place while any overlay references it.
Before you begin
Complete Episode 1: Install KVM on Ubuntu, or verify the equivalent state:
virsh --connect qemu:///system uri
virsh --connect qemu:///system net-info default
command -v qemu-img
command -v virt-install
Install the cloud-image utility if cloud-localds is missing:
sudo apt update
sudo apt install -y cloud-image-utils
command -v cloud-localds
You also need an SSH public key. The command below must print a .pub file; do not use the private key path without the suffix:
ls -l ~/.ssh/*.pub
1. Set the guest paths
Use explicit names so validation and cleanup target only this lab guest:
VM=demo-ubuntu01
BASE=/var/lib/libvirt/images/ubuntu-24.04-server-cloudimg-amd64.img
DISK=/var/lib/libvirt/images/${VM}.qcow2
SEED=/var/lib/libvirt/images/${VM}-seed.iso
Refuse to continue if the domain already exists:
if virsh --connect qemu:///system dominfo "$VM" >/dev/null 2>&1; then
echo "$VM already exists; choose another name or inspect it first"
exit 1
fi
Do not overwrite a domain or disk merely because a tutorial uses the same example name.
2. Download the Ubuntu cloud image
Download the current Ubuntu 24.04 LTS amd64 cloud image:
sudo wget -O "${BASE}.download" \
https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img
sudo mv "${BASE}.download" "$BASE"
Inspect it before using it as a backing file:
sudo qemu-img info "$BASE"
For a production image pipeline, pin and verify a published checksum rather than silently accepting a changing current image.
3. Create a qcow2 overlay
Create a sparse 24 GiB guest disk backed by the downloaded image:
sudo qemu-img create \
-f qcow2 \
-F qcow2 \
-b "$BASE" \
"$DISK" \
24G
Confirm the virtual size and backing file:
sudo qemu-img info "$DISK"
The overlay may occupy only a few hundred kilobytes initially even though its virtual capacity is 24 GiB. It grows as the guest writes data.
4. Create cloud-init user-data
Create demo-user-data.yaml and replace the placeholder with the contents of your public key file:
#cloud-config
hostname: demo-ubuntu01
manage_etc_hosts: true
users:
- default
- name: demo
gecos: InventiveHQ Tutorial Guest
groups: [adm, sudo]
shell: /bin/bash
sudo: ALL=(ALL) NOPASSWD:ALL
lock_passwd: true
ssh_authorized_keys:
- REPLACE_WITH_YOUR_PUBLIC_SSH_KEY
packages:
- qemu-guest-agent
runcmd:
- [systemctl, start, qemu-guest-agent]
- [systemctl, enable, --now, serial-getty@ttyS0.service]
final_message: "demo-ubuntu01 is ready"
Cloud-init will create the demo account, install the guest agent, and enable a serial login prompt. lock_passwd: true keeps password login disabled for that account.
Create demo-meta-data.yaml:
instance-id: demo-ubuntu01-20260716
local-hostname: demo-ubuntu01
If you reuse the disk but want cloud-init to run as a new instance, change the instance ID deliberately. Changing it on an existing workload can re-run initialization behavior and should not be treated casually.
5. Build the NoCloud seed ISO
Generate the small read-only disk that supplies user-data and meta-data on first boot:
sudo cloud-localds "$SEED" demo-user-data.yaml demo-meta-data.yaml
sudo ls -lh "$SEED"
Give libvirt's QEMU process access to the image files using your distribution's expected ownership and security policy. On the verified Ubuntu host:
sudo chown libvirt-qemu:kvm "$BASE" "$DISK" "$SEED"
sudo chmod 0640 "$BASE" "$DISK" "$SEED"
Do not disable AppArmor or make the image directory world writable to work around a path or ownership error.
6. Define and start the VM
Create the guest from the prepared disk:
sudo virt-install \
--connect qemu:///system \
--name "$VM" \
--memory 2048,maxmemory=4096 \
--vcpus 2,maxvcpus=4 \
--cpu host-model \
--osinfo ubuntu24.04 \
--import \
--disk path="$DISK",format=qcow2,bus=virtio \
--disk path="$SEED",device=cdrom \
--network network=default,model=virtio \
--graphics none \
--console pty,target_type=serial \
--channel unix,target.type=virtio,target.name=org.qemu.guest_agent.0 \
--noautoconsole
Important choices in this definition:
--importboots the already-installed cloud image instead of running an ISO installer.- The active and maximum CPU/memory values create safe headroom for Episode 3.
- Virtio provides efficient disk and network devices.
- The default network gives private DHCP and NAT.
- The serial device provides recovery when SSH or guest networking is unavailable.
- The guest-agent channel lets libvirt communicate with
qemu-guest-agentafter cloud-init installs it.
7. Watch first boot and verify cloud-init
Confirm that the domain exists and is running:
virsh --connect qemu:///system list --all
virsh --connect qemu:///system dominfo "$VM"
Find its address through the libvirt DHCP lease while the guest agent is still installing:
virsh --connect qemu:///system domifaddr "$VM" --source lease
Connect over SSH, substituting the reported address:
ssh demo@192.168.122.40
cloud-init status --wait
systemctl is-active qemu-guest-agent
systemctl is-enabled serial-getty@ttyS0.service
The IP address is an example from the lab, not a promise. Query the lease on your host.
After the guest agent becomes active, this should report the address from inside the VM:
virsh --connect qemu:///system domifaddr "$VM" --source agent
8. Verify the recovery console and resource headroom
Find and open the serial console:
virsh --connect qemu:///system ttyconsole "$VM"
virsh --connect qemu:///system console "$VM" --safe
Press Enter if the login prompt is not immediately visible. Disconnect with Ctrl+].
Verify the active and maximum vCPU counts:
virsh --connect qemu:///system vcpucount "$VM"
The expected result is 2 current and 4 maximum for both live and persistent configuration. Inspect the memory definition too:
virsh --connect qemu:///system dumpxml "$VM" --inactive \
| grep -E '<(memory|currentMemory|vcpu)'
Enable autostart only after the guest passes validation:
virsh --connect qemu:///system autostart "$VM"
virsh --connect qemu:///system dominfo "$VM" | grep Autostart
Troubleshooting
virt-install reports permission denied for an image
Check every parent directory, the file owner, and the host security policy:
namei -l "$DISK"
sudo journalctl -u libvirtd -n 100 --no-pager
sudo dmesg | grep -i apparmor | tail
Keep images in a libvirt-managed path and fix deliberate ownership or policy configuration. Do not use chmod 777 as a shortcut.
The guest has no IP address
Verify the domain interface and network:
virsh domiflist "$VM"
virsh net-info default
virsh net-dhcp-leases default
If you redefine a cloud-image guest after first boot, preserve its MAC address or update the guest network configuration. Cloud-init may have generated configuration tied to the original interface.
The guest-agent query fails
First boot can finish networking before package installation completes. Use the lease source, then check inside the guest:
cloud-init status --wait
sudo systemctl status qemu-guest-agent --no-pager
Also confirm that the domain includes the channel named org.qemu.guest_agent.0.
Cleanup for a disposable rerun
Do not run this against a guest you want to keep. Inspect the exact paths first:
virsh domblklist "$VM" --details
virsh shutdown "$VM"
virsh undefine "$VM"
sudo rm -f "$DISK" "$SEED"
The base image can remain for other overlays. Remove it only after proving that no remaining qcow2 disk references it.
Continue to Episode 3
Keep demo-ubuntu01 running. In Episode 3: Manage KVM Virtual Machines with virsh, we inventory this guest, export its persistent definition, increase its active and persistent resources, verify the result inside Ubuntu, use the serial recovery path, and distinguish graceful lifecycle operations from destructive ones.