Phase 1: KVM VM Creation

Phase 1: KVM VM Creation

Create the RHEL 9 workstation VM on your local KVM infrastructure. All operations via virsh and virt-install — no virt-manager GUI.

Prerequisites (on KVM host)

# Verify KVM is available
lscpu | grep -i virtualization
# Check modules
lsmod | grep kvm
# Verify libvirt running
systemctl is-active libvirtd
# Your user in libvirt group
groups | grep -E '(libvirt|kvm)'
# If not in group:
sudo usermod -aG libvirt,kvm $(whoami) && newgrp libvirt

Storage Pool

# Create dedicated storage directory
sudo mkdir -p /var/lib/libvirt/images/rhel9-workstation
# Create root disk (40GB — LVM will manage partitions)
sudo qemu-img create -f qcow2 /var/lib/libvirt/images/rhel9-workstation/root.qcow2 40G
# Create additional disks for LVM/Stratis labs
sudo qemu-img create -f qcow2 /var/lib/libvirt/images/rhel9-workstation/data1.qcow2 10G
sudo qemu-img create -f qcow2 /var/lib/libvirt/images/rhel9-workstation/data2.qcow2 10G
# Verify all disks created
ls -lh /var/lib/libvirt/images/rhel9-workstation/

Network: Bridge + VLAN Hook

The VM needs a bridged network on your lab VLAN — not NAT. You’ve done this before with your Rocky KVM hosts.

Verify Existing Bridge

# List bridges on KVM host
ip link show type bridge
# List libvirt networks
virsh net-list --all
# Show bridge details (if br0 exists)
ip -4 -o addr show br0 | awk '{print $2, $4}'

VLAN Hook (if needed)

If the VM needs a specific VLAN tag and you’re using the qemu hook pattern from your Rocky KVM builds:

# Check if VLAN hook exists
cat /etc/libvirt/hooks/qemu 2>/dev/null | head -20
# If hook doesn't exist, create it (adjust VLAN ID and bridge to your lab)
sudo tee /etc/libvirt/hooks/qemu << 'HOOKEOF'
#!/bin/bash
# VLAN persistence hook — assigns VMs to correct VLAN on start
# Triggered by libvirt on VM start/stop/migrate events

GUEST_NAME="$1"
ACTION="$2"

case "${GUEST_NAME}" in
    rhel9-workstation)
        VLAN_ID=10    # Data VLAN
        BRIDGE="br0"
        ;;
    *)
        exit 0
        ;;
esac

if [ "${ACTION}" = "started" ]; then
    # Find the VM's tap interface
    TAP=$(virsh domiflist "${GUEST_NAME}" | awk '/bridge/{print $1}')
    if [ -n "${TAP}" ]; then
        bridge vlan add dev "${TAP}" vid "${VLAN_ID}" pvid untagged
        logger "qemu-hook: ${GUEST_NAME} tap=${TAP} vlan=${VLAN_ID}"
    fi
fi
HOOKEOF
sudo chmod +x /etc/libvirt/hooks/qemu
# Restart libvirtd to pick up hook
sudo systemctl restart libvirtd
Adjust the VLAN ID, bridge name, and VM-to-VLAN mapping to match your lab. If your bridge already trunks the correct VLANs, you may not need the hook — the VM gets a real IP via DHCP on the bridge’s native VLAN.

Create VM

sudo virt-install \
  --name rhel9-workstation \
  --ram 8192 \
  --vcpus 4 \
  --cpu host-passthrough \
  --disk path=/var/lib/libvirt/images/rhel9-workstation/root.qcow2,bus=virtio \
  --disk path=/var/lib/libvirt/images/rhel9-workstation/data1.qcow2,bus=virtio \
  --disk path=/var/lib/libvirt/images/rhel9-workstation/data2.qcow2,bus=virtio \
  --os-variant rhel9.0 \
  --cdrom ~/isos/rhel-9.*-x86_64-dvd.iso \
  --network bridge=br0,model=virtio \
  --graphics vnc,listen=0.0.0.0,port=5901 \
  --video virtio \
  --console pty,target_type=serial \
  --boot uefi \
  --noautoconsole

Key flags explained:

Flag Why

--cpu host-passthrough

Expose real CPU features to VM (better performance, required for some workloads)

--network bridge=br0,model=virtio

Bridged to lab VLAN with virtio NIC (fastest paravirtualized driver)

--graphics vnc,listen=0.0.0.0,port=5901

VNC for initial install only — accessed via Cockpit VM console or browser

--video virtio

virtio-gpu (better than QXL for modern guests)

--console pty,target_type=serial

Serial console for virsh console access post-install (terminal-only management)

--boot uefi

UEFI boot (matches modern hardware, GRUB2 with EFI)

Adjust --network bridge=br0 to match your KVM host bridge. If your VLAN hook handles tagging, the bridge just needs to be a trunk port.

Connect to Installer

You need graphical access for the Anaconda installer only (custom LVM partitioning requires the GUI — text mode inst.text doesn’t support it well). Three options:

# Enable Cockpit on KVM host if not already
sudo systemctl enable --now cockpit.socket

Navigate to kvm-host:9090 → Virtual Machines → rhel9-workstation → Console. Works in your browser on Hyprland.

Option 2: virt-viewer (GTK app — works on Wayland/Hyprland)

# From your Hyprland workstation
virt-viewer --connect qemu+ssh://user@kvm-host/system rhel9-workstation

Option 3: VNC via browser

# Get VNC port
virsh vncdisplay rhel9-workstation

Connect with any VNC client or noVNC in browser.

Post-Install: Enable Serial Console

After RHEL 9 is installed, enable serial console for virsh console access (terminal-only, no VNC needed):

# On the RHEL 9 VM (via SSH or initial VNC session):
# Enable serial console in GRUB
sudo grubby --update-kernel=ALL --args="console=ttyS0,115200n8"
# Enable serial getty
sudo systemctl enable serial-getty@ttyS0.service
sudo systemctl start serial-getty@ttyS0.service
# Verify
sudo systemctl is-enabled serial-getty@ttyS0.service

After reboot, you can access the VM with:

# From KVM host — pure terminal, no graphics
virsh console rhel9-workstation
# Ctrl+] to exit

VM Management (virsh Reference)

Task Command

List all VMs

virsh list --all

Start

virsh start rhel9-workstation

Graceful shutdown

virsh shutdown rhel9-workstation

Force stop

virsh destroy rhel9-workstation

Reboot

virsh reboot rhel9-workstation

Serial console

virsh console rhel9-workstation

Get IP

virsh domifaddr rhel9-workstation

Get MAC

virsh domiflist rhel9-workstation

VM info

virsh dominfo rhel9-workstation

CPU/memory stats

virsh domstats rhel9-workstation

Edit XML

virsh edit rhel9-workstation

Snapshots

Task Command

Create snapshot

virsh snapshot-create-as rhel9-workstation --name "label" --description "notes"

List snapshots

virsh snapshot-list rhel9-workstation

Show snapshot info

virsh snapshot-info rhel9-workstation --snapshotname "label"

Revert to snapshot

virsh snapshot-revert rhel9-workstation --snapshotname "label"

Delete snapshot

virsh snapshot-delete rhel9-workstation --snapshotname "label"

Disk Management

Task Command

List disks

virsh domblklist rhel9-workstation

Add disk (hot)

virsh attach-disk rhel9-workstation /path/to/disk.qcow2 vdX --persistent

Remove disk

virsh detach-disk rhel9-workstation vdX --persistent

Create disk image

qemu-img create -f qcow2 /path/to/disk.qcow2 10G

Disk info

qemu-img info /path/to/disk.qcow2

Resize disk

qemu-img resize /path/to/disk.qcow2 +5G

Network Management

Task Command

List interfaces

virsh domiflist rhel9-workstation

Interface stats

virsh domifstat rhel9-workstation vnet0

Autostart on boot

virsh autostart rhel9-workstation

Disable autostart

virsh autostart --disable rhel9-workstation

Dump XML config

virsh dumpxml rhel9-workstation

Delete VM + storage

virsh undefine rhel9-workstation --remove-all-storage --nvram

Check Status

KVM/libvirt verified on host

[ ]

Storage pool and disks created (40GB root + 2x 10GB data)

[ ]

Network bridge configured (bridged, not NAT)

[ ]

VLAN hook in place (if needed)

[ ]

VM created with virt-install

[ ]

Installer accessible (Cockpit console / virt-viewer / VNC)

[ ]

Serial console enabled post-install (grubby + serial-getty)

[ ]

virsh console works from KVM host

[ ]

VM set to autostart (virsh autostart)

[ ]