Create qemu-kvm guest

Synopsis

On an internet-connected x86_64 host running CentOS Linux 7.9 with qemu-kvm and dependencies installed (see Set up qemu-kvm for CentOS 7):

Preliminary requirements

To create and run VMs as a non-root user your USER account must be a member of the kvm and qemu groups.

Create a private workspace:

> mkdir -p ${HOME}/qemu/{cfg,efi,img,pki,run}

Guest VM network options

Private network -netdev user

This is the default; each VM is in its own private network with non-routable ip address(es) assigned to the guest's NIC(s) by the built-in qemu DHCP server:
Default network: 10.0.2.0/24
Gateway (host) address: 10.0.2.2
DNS server address: 10.0.2.3
Guest address range: 10.0.2.15 to 10.0.2.31

Set alternative and additional params at the -netdev command line. You must add the 'net=', 'host=', 'dhcpstart=' and 'dns=' params if using a pre-set $MACADDR for the NIC.

Reference: Qemu Documentation/Networking

Public network -netdev tap

For a VM to have full internet connectivity with a public ip address your USER account must have permission to attach the VM's NIC to a tap device (e.g. tap_$USER connected to the host bridge (e.g. br0). Note that the following will not persist between reboots:

> sudo ip tuntap add dev tap_${USER} mode tap user ${USER}
> sudo ip link set dev tap_${USER} up
> sudo ip link set tap_${USER} master br0

Reference: Hosting QEMU VMs with Public IP Addresses using TAP Interfaces

Create guest VM from iso file

Retrieve a Linux installation iso image from a repository; we selected a Debian 11.5 ("Bullseye") iso image; it contains non-free firmware required to set up UEFI boot inside the VM:

> ISO_URL='https://cdimage.debian.org/cdimage/unofficial/non-free/cd-including-firmware/11.5.0-live+nonfree/amd64/iso-hybrid/'
> ISO_FILE='debian-live-11.5.0-amd64-standard+nonfree.iso'
> curl -k -L -s -o ${HOME}/qemu/img/${ISO_FILE} ${ISO_URL}/${ISO_FILE}

Set a name for the guest VM:

> VM_NAME="debian11"

Copy OVMF UEFI boot config overlay file to private workspace:

> cp /usr/share/OVMF/OVMF_VARS.fd ${HOME}/qemu/efi/${VM_NAME}.fd

Set vars for UEFI file paths:

EFI_CODE="/usr/share/OVMF/OVMF_CODE.secboot.fd"
EFI_VARS="${HOME}/qemu/efi/${VM_NAME}.fd"
EFI_SHELL="/usr/share/OVMF/UefiShell.iso"

Create an empty (sparse) 10G disk image in qcow2 format:

> qemu-img create -f qcow2 ${HOME}/qemu/img/${VM_NAME}.qcow2 10G

Set qemu-kvm command line params

Set name and assign full host cpu capabilities with 2G RAM to guest VM:

-name ${VM_NAME} -cpu host -smp $(nproc) -m 2048M

Emulate Intel Q35 Express chipset, enable kvm and boot with OVMF UEFI images; EFI_CODE must be declared before EFI_VARS; suppress iPXE boot with -net none; for ancient OSes use Intel 440fx chipset (does not support UEFI, legacy BIOS only):

-machine type=q35,accel=kvm -enable-kvm
-drive if=pflash,index=0,format=raw,read-only=on,file=${EFI_CODE}
-drive if=pflash,index=1,format=raw,file=${EFI_VARS}
-net none

Set hard disk and cdrom drives; do not use virtio drivers (Debian installer will default to scsi) else an EFI system partition (ESP) will not be created in the guest:

-cdrom ${HOME}/qemu/img/${ISO_FILE}
-hda ${HOME}/qemu/img/${VM_NAME}.qcow2

Output guest VM display to qxl-vga (enhanced VGA) device using qemu's built-in vnc server, listen for connections on port 6001 of all interfaces, no password, use US English keyboard; enable USB tablet to align mouse pointer with vnc display:

-device qxl-vga -display vnc=0.0.0.0:101 -k en-us
-device qemu-xhci -device usb-tablet

Reference: Qemu USB emulation

Create qemu monitor and serial port Unix sockets:

-chardev socket,id=console,path=${HOME}/qemu/run/${VM_NAME}.console,server,nowait
-serial chardev:console
-chardev socket,id=monitor,path=${HOME}/qemu/run/${VM_NAME}.monitor,server,nowait
-monitor chardev:monitor

References:
Set up a serial terminal/console in RHEL
QEMU - Send serial data from host to guest

Set up guest VM network; generate a unique MAC address; create guest NIC; do not use virtio driver for ancient OSes:

for COUNT in {1..6}; do
  OCTET="$(tr -cd 1234567890ABCDEF </dev/urandom | head -c2)"
  [[ $COUNT -lt 6 ]] && OCTET="${OCTET}:"
  MACADDR="${MACADDR}${OCTET}"
done

-device virtio-net-pci,netdev=network,mac=${MACADDR}

For a public network (params set in guest):

-netdev tap,id=network,ifname=tap_${USER},script=no,downscript=no

For a private network (params set for $MACADDR):

-netdev user,id=network,net=10.0.2.0/24,host=10.0.2.2,dhcpstart=10.0.2.15,dns=10.0.2.3

The first entry in the host's /etc/resolv.conf file will become the guest's name server; if that's not a public (internet-accessible) name server, the guest will be unable to resolve domain names. In that case, edit the guest's /etc/resolv.conf file and replace the entry nameserver 10.0.2.3 with one or more public name servers, e.g nameserver 8.8.8.8 or nameserver 9.9.9.9

Create pidfile if starting the VM from a script:

-pidfile ${HOME}/qemu/run/${VM_NAME}.pid

Start the VM

Insert all params on the command line; use -daemonize as last param to detach the process from stdin/stdout and run it in the background:

> /usr/libexec/qemu-kvm -name ${VM_NAME} ... -daemonize

Install Debian 11 from iso image

Connect to the guest VM display:

> vncviewer localhost::6001 2>/dev/null &

The vnc server runs on the host not the guest. To connect to the guest VM display from a different computer replace localhost with the host's actual ip address; the host's firewall must permit connections on port 6001.

Connect to VM serial port and Qemu monitor

Use socat for an interactive connection to the VM serial port and Qemu monitor socket. The following will connect to the monitor socket, disable the default ^C and ^D quit/exit escape sequences and enable the telnet ^] quit/exit escape sequence:

> socat stdio,rawer,escape=0x1d unix-connect:${HOME}/qemu/run/${VM_NAME}.monitor

Reference: Socat, Telnet and Unix sockets

Use netcat or socat for scripted connections. The following will shut down the VM from the command line using netcat (nc) or socat:

> printf '%b' 'system_powerdown\n' | nc -U ${HOME}/qemu/run/${VM_NAME}.monitor
> printf '%b' 'system_powerdown\n' | socat stdio unix-connect:${HOME}/qemu/run/${VM_NAME}.monitor