2.4 · Getting a guest running
virt-install Explained: Every Flag for an arm64 Guest
The previous post cleared the four problems that
stopped my first VM from being created. This post explains the command that finally worked.
It built lab1, a Debian guest on home1, the SD card for my Raspberry Pi 5 that Part 2
was done on:
virt-install \
--connect qemu:///system \
--name lab1 \
--arch aarch64 \
--machine virt \
--ram 2048 \
--vcpus 2 \
--disk path=/var/lib/libvirt/images/lab1.qcow2,size=12,format=qcow2,bus=virtio \
--os-variant debian11 \
--network network=default,model=virtio \
--graphics none \
--console pty,target_type=serial \
--boot loader=/usr/share/AAVMF/AAVMF_CODE.fd,loader.readonly=yes,loader.type=pflash,nvram.template=/usr/share/AAVMF/AAVMF_VARS.fd \
--location 'https://deb.debian.org/debian/dists/bookworm/main/installer-arm64/' \
--extra-args 'console=ttyAMA0'
Copying it works. Understanding it means you can write the next one yourself, for a different operating system or a different way of installing it, without looking for a template.
The way to do that is not to memorise fourteen options. It is to see that they answer seven questions, in order.
What virt-install is
First, where it fits. As post 1.3 described,
virsh is libvirt’s main command-line tool, but it works on VMs that already exist. Creating one
from nothing would mean writing its whole XML definition by hand.
virt-install fills that gap. It takes a short description of the machine, writes the full XML,
hands it to libvirtd, and starts the installation:
| Step | What happens |
|---|---|
| 1 | You describe the machine with options |
| 2 | virt-install turns them into a complete XML definition |
| 3 | It hands that XML to libvirtd, through libvirt’s interface |
| 4 | libvirtd turns the XML into a qemu-system-aarch64 command, and starts QEMU |
| 5 | QEMU starts the VM: first the UEFI firmware, then the installer |
| 6 | virt-install connects your terminal to the VM’s serial console |
Notice steps 2 and 3. virt-install is a convenience that writes XML. It never talks to QEMU
directly, and never touches KVM. Everything in Part 1 still holds: this tool sits on top, and saves
you writing the document by hand.
Two behaviours to expect. It both defines and starts the VM, taking you straight into the
installer, rather than only saving a definition. And it connects your terminal to the guest’s serial
console, so your terminal becomes the VM’s screen. Ctrl + ] disconnects without stopping the VM,
and virsh console lab1 reconnects.
There is also --print-xml, which writes the XML and prints it, without creating anything. It is useful
for seeing exactly what a set of options produces, and post 2.3 used it to show an error safely.
From options to XML
Every option ends up as part of the VM’s XML. Here is where each one landed in lab1’s real definition,
which post 3.4 reads block by block:
The seven questions
Every option answers one of these:
- Which libvirt does the VM belong to?
- What is it called, and what shape is its hardware?
- What disk does it use?
- How does it reach the network?
- How do you see it and type to it?
- How does it start?
- Which operating system goes on it, and how does it get there?
Here they are in turn.
1. Which libvirt: —connect
--connect qemu:///system
This says which libvirt owns the VM. Post 2.3 explained why it matters:
qemu:///system and qemu:///session are completely separate, and the default network only exists in the first.
So this option is not about the command. It decides where the VM lives for its whole life. Get it wrong, and the
VM is built in a libvirt with no network, and then seems to be missing from every later virsh command. Even with
LIBVIRT_DEFAULT_URI set in your shell, it is worth giving here.
2. Name and hardware: —name, —arch, —machine, —ram, —vcpus
--name lab1
--arch aarch64 --machine virt
--ram 2048 --vcpus 2
--name is how every later virsh command refers to the VM. It becomes <name> in the XML.
--arch aarch64 sets the guest’s processor architecture. It defaults to the host’s, so on a Pi it changes nothing,
but it is a good habit, and becomes essential if you ever deliberately emulate a different architecture. As
post 1.2 explained, a guest whose architecture does not
match the host cannot use KVM at all.
--machine virt chooses the virtual motherboard. As post 1.4
described, x86 VMs imitate real chipsets, such as pc and q35, while ARM64 uses virt, an artificial board designed for
VMs, with no old hardware to imitate. On ARM64 you almost always want virt. libvirt records the exact version, virt-7.2, so
the VM’s hardware does not change when QEMU is upgraded.
--ram 2048 --vcpus 2 give the VM 2,048 MB of memory and two virtual processors. (--memory is the newer name for --ram;
both work.) On an 8 GB Pi with four cores, that leaves plenty for the host. Each virtual processor becomes a thread in the QEMU
process, as post 1.3 described.
3. The disk: —disk
--disk path=/var/lib/libvirt/images/lab1.qcow2,size=12,format=qcow2,bus=virtio
The densest option, so learn how it is built rather than the string itself. It is a list of key=value settings, separated by
commas, describing one virtual disk:
| Setting | Means |
|---|---|
path= | Where the disk file is kept. /var/lib/libvirt/images/ is libvirt’s default storage pool. If the file does not exist, libvirt creates it, using qemu-img. If it does, it is used as it is. |
size=12 | The size, in GiB, used only when creating the file. With qcow2 this is the most the disk can hold, not space set aside: the file starts small, and grows as the guest writes. |
format=qcow2 | The disk file’s format, which grows as it is filled and supports snapshots. The alternative, raw, is a plain copy of every sector. Part 4 explains both. |
bus=virtio | How the guest sees the disk: as a virtio disk, from post 1.6, rather than an imitation of real hardware. This is why the guest calls it /dev/vda, not /dev/sda. For any modern Linux guest, use virtio. |
Four questions: which file, how big, in which format, on which bus.
4. The network: —network
--network network=default,model=virtio
network=default plugs the VM into libvirt’s network called default: the NAT network on the virbr0 bridge, with dnsmasq
giving out addresses. This is the reference that fails if that network is not running, which is the port 53 problem from post 2.3.
model=virtio chooses a virtio network card, for the same reason as the disk. The alternative, e1000, imitates a real Intel
card, and is there for guests without virtio drivers.
Other kinds of network, such as bridged and macvtap, are covered in Part 5. network=default,model=virtio is the everyday choice.
5. Seeing and typing: —graphics, —console
--graphics none
--console pty,target_type=serial
--graphics none means no virtual screen at all, which suits a Pi you reach over SSH. In the XML, there is simply no <graphics>
element.
--console pty,target_type=serial adds a serial console and connects your terminal to it. pty means the host’s end is a
pseudo-terminal, and target_type=serial makes the guest’s end a serial port. Post 3.6
explains why the XML then describes this one console twice.
It is worth knowing what this costs. As post 1.5 explained, each character the guest writes to its imitation serial port causes a VM exit, and a trip into QEMU. That is fine for typing at a login prompt, and it is why nobody moves large amounts of data over a serial console.
6. How it starts: —boot
--boot loader=/usr/share/AAVMF/AAVMF_CODE.fd,loader.readonly=yes,loader.type=pflash,nvram.template=/usr/share/AAVMF/AAVMF_VARS.fd
The option specific to ARM64, and the fix for the Security Violation in post 2.3. ARM64 VMs start with UEFI firmware rather than the old BIOS.
libvirt normally chooses the firmware itself, but here it chose the version that enforces Secure Boot, which refuses Debian’s unsigned ARM64 installer.
This option names the plain firmware instead.
| Setting | Means |
|---|---|
loader= | The firmware program: the plain AAVMF_CODE.fd, without Secure Boot |
loader.readonly=yes | The program is shared, read-only, by every VM |
loader.type=pflash | It is loaded as flash memory, which is where UEFI expects to be |
nvram.template= | The clean template this VM’s own writable settings file is copied from |
UEFI firmware comes in two parts, and that explains the option: CODE, the program, which is read-only and shared, and VARS, the settings, including the boot entries and Secure Boot state, which each VM has its own copy of. Post 3.3 goes through that split in detail.
On x86, this option is usually unnecessary.
7. The operating system: —location, —os-variant, —extra-args
--location 'https://deb.debian.org/debian/dists/bookworm/main/installer-arm64/'
--os-variant debian11
--extra-args 'console=ttyAMA0'
--location points at a network installation tree: a folder on a server, from which virt-install downloads a Linux kernel and its initial
files, and uses them to start the installer. On home1, it downloaded linux, 31 MB, and initrd.gz, 38 MB. The address has to be the tree for
ARM64.
--os-variant only provides hints for the default hardware. As post 2.3 explained, debian11 is fine with an older database: the operating system
actually installed comes from --location, which here is Debian 12.
--extra-args adds options to the installer kernel’s command line. console=ttyAMA0 tells the guest’s kernel to send its output to the ARM serial
port, so it reaches your terminal. This is the partner of --console: that option creates the serial port, and this one tells the guest to use it. On x86,
the equivalent is ttyS0. Because the whole installation then runs over the serial console, Debian keeps that setting in the installed system too, which is
why virsh console lab1 later shows a login prompt.
The outline
Remember this rather than the full command:
virt-install \
--connect <which libvirt> \ # 1. where it lives
--name <name> --arch <arch> --machine <board> \ # 2. name and hardware
--ram <MB> --vcpus <count> \ # size
--disk path=...,size=...,format=...,bus=virtio \ # 3. disk
--network network=...,model=virtio \ # 4. network
--graphics none --console pty,target_type=serial \ # 5. console
--boot loader=...CODE.fd,...,nvram.template=...VARS.fd \ # 6. firmware
--location <url> --os-variant <name> \ # 7. operating system
--extra-args 'console=ttyAMA0' # its console
Seven questions. The options are just the answers.
Testing the model: a different operating system
The real test of having learned the structure, rather than the string, is writing the command for a different guest. Alpine Linux is a good case, because it shows exactly where the model holds, and where it does not.
My first attempt kept everything from the Debian command, and changed the last group: --os-variant alpine-rpi-3, a --location pointing at a file called
alpine-rpi-3.24.1-aarch64.img.gz, and the same --extra-args 'console=ttyAMA0'.
Questions 1 to 6 carried over unchanged: which libvirt, the name and hardware, the disk, the network, the console and the firmware are all the same, because it is the same kind of virtual machine. That part was right, and it is the whole point of the model.
Question 7 was wrong in three separate ways, and each is instructive:
--locationwas the wrong mechanism. It expects an installation tree, a kernel and initial files that start an installer over the network, which is how Debian provides its installer. Alpine does not provide one in that form. It provides bootable ISO images, which means--cdrom.- The file was wrong too.
alpine-rpiimages are for writing to an SD card for a physical Raspberry Pi, not for installing into a VM. The right file is thealpine-virtISO, the version built for virtual machines. --os-variant alpine-rpi-3is not a valid name, and this database is older than current Alpine anyway.genericis the safe choice.
And --extra-args silently does nothing with --cdrom: it only works with --location, where virt-install controls the kernel’s command line. With an ISO, the ISO’s
own bootloader runs, and the option is ignored.
The corrected command, from my notes:
virt-install \
--connect qemu:///system \
--name alpine \
--arch aarch64 --machine virt \
--ram 1024 --vcpus 1 \
--disk path=/var/lib/libvirt/images/alpine.qcow2,size=8,format=qcow2,bus=virtio \
--os-variant generic \
--network network=default,model=virtio \
--graphics none \
--console pty,target_type=serial \
--boot loader=/usr/share/AAVMF/AAVMF_CODE.fd,loader.readonly=yes,loader.type=pflash,nvram.template=/usr/share/AAVMF/AAVMF_VARS.fd \
--cdrom 'https://dl-cdn.alpinelinux.org/alpine/v3.24/releases/aarch64/alpine-virt-3.24.1-aarch64.iso'
The installation itself is different too. Debian’s installer is a guided menu. Alpine starts a small live system, where you log in as root, with no password, and run
setup-alpine, a script that asks about the keyboard, hostname, network, root password, time zone, package mirror and users, and then the disk, where you choose sys
mode and the disk vda to install permanently.
Afterwards, eject the ISO so the VM starts from its disk. The CD drive is called sda, as virsh domblklist alpine shows:
virsh change-media alpine sda --eject --config
virsh start alpine
virsh console alpine
The alpine VM used in Parts 4 and 5 is an Alpine guest of this kind: one virtual processor, 1,024 MB of memory, and an empty CD drive, sda, though with a 2 GiB disk
rather than the 8 GiB in this command.
The line between the machine and its installation
Questions 1 to 6 stayed the same between the Debian and Alpine commands. Only question 7 changed, because only the way of installing changed.
That is the dividing line: the machine’s definition and the installation method are independent. The three common installation methods are:
| Method | Option | What follows |
|---|---|---|
| A network installer, like Debian’s | --location <installation tree>, plus --extra-args | A guided installer |
| A bootable ISO, like Alpine’s | --cdrom <ISO> | Setting up by hand, such as with setup-alpine |
| A cloud image | --import, with a disk that already has the operating system on it | No installation at all |
The third is worth knowing about, because it is what production systems use. A cloud image is a disk file with the operating system already installed: you download it and start it, with no installation. It has no user set up, so it relies on a tool called cloud-init to add an account and an SSH key when it first starts. Doing a manual installation first is what makes that make sense when you meet it.
Summary
virt-installwrites a VM’s XML from its options, hands it tolibvirtd, and starts the installer. It never talks to QEMU or KVM directly.- It both defines and starts the VM, and connects your terminal to its serial console.
Ctrl + ]disconnects. - Every option answers one of seven questions: which libvirt, name and hardware, disk, network, console, firmware, and operating system.
- Each option becomes part of the XML:
--ram 2048becomes<memory unit='KiB'>2097152</memory>,--graphics nonemeans no<graphics>element, and so on. bus=virtioandmodel=virtiochoose virtio devices. A virtio disk appears as/dev/vda.--consolecreates the serial port, and--extra-args 'console=ttyAMA0'tells the guest to use it. Both are needed.- The
--bootoption is specific to ARM64, and avoids libvirt choosing firmware that enforces Secure Boot. - Changing the operating system usually only changes question 7.
--locationneeds an installation tree,--cdromneeds an ISO, and--extra-argsonly works with--location. virsh change-medianeeds the CD drive’s name, such assda, to eject an ISO.
Comments