Stackvora.tech

Create a VM From the Command Line With virt-install

Provisioning virtual machines efficiently and repeatably is a core task for sysadmins and infrastructure engineers. While graphical tools like…

Provisioning virtual machines efficiently and repeatably is a core task for sysadmins and infrastructure engineers. While graphical tools like virt-manager offer convenience, the command line provides automation, precision, and integration into scripting workflows. The virt-install utility, part of the libvirt ecosystem, is the go-to tool for creating KVM/QEMU virtual machines programmatically. This article details its robust capabilities for both interactive and unattended VM deployments.

We will cover common installation methods, disk configurations, network setups, and the crucial role of cloud-init for automated provisioning, focusing on Ubuntu 22.04 LTS as a practical example. All examples assume a host system with KVM/QEMU and libvirt installed and configured, typically running a modern Linux distribution like Ubuntu Server, CentOS Stream, or Fedora.

Understanding virt-install Fundamentals

virt-install is a command-line tool for creating new virtual machines, domains, or defining existing domains in the libvirt virtualization management system. It supports a wide range of installation methods, from local ISOs to network-based installations via HTTP, FTP, or NFS, and even PXE boot. Its power lies in its extensive set of options, allowing fine-grained control over every aspect of VM creation.

Essential Options for Basic VM Creation

At its core, a virt-install command needs a few key pieces of information:

  • --name <VM_NAME>: A unique name for your virtual machine.
  • --memory <MB>: The amount of RAM, in megabytes, to allocate.
  • --vcpus <COUNT>: The number of virtual CPUs to assign.
  • --disk path=<PATH>,size=<GB>,format=<FORMAT>: Specifies the disk image. You can either provide a path to an existing image or specify a size in GB for virt-install to create one. Common formats include qcow2 (recommended for features like snapshots and sparse allocation) and raw.
  • --os-variant <VARIANT>: Optimizes guest settings based on the operating system. Use osinfo-query os to list available variants (e.g., ubuntu22.04, rhel8.4).

For a minimal interactive installation using an ISO image:

sudo virt-install \
    --name my-ubuntu-vm \
    --memory 2048 \
    --vcpus 2 \
    --disk path=/var/lib/libvirt/images/my-ubuntu-vm.qcow2,size=20,format=qcow2 \
    --cdrom /path/to/ubuntu-22.04-live-server-amd64.iso \
    --network network=default \
    --os-variant ubuntu22.04 \
    --graphics vnc,listen=0.0.0.0 \
    --noautoconsole

This command creates a VM named my-ubuntu-vm with 2GB RAM, 2 vCPUs, a 20GB QCOW2 disk, and boots from the specified ISO. It attaches to the default libvirt network and exposes a VNC console accessible from any IP address on the host (listen=0.0.0.0). --noautoconsole prevents virt-install from automatically connecting to the serial console, allowing you to connect via VNC (e.g., using virt-viewer --connect qemu:///system my-ubuntu-vm).

Disk Configuration and Storage Management

Disk configuration is critical. virt-install provides flexible options.

Creating New Disk Images

The simplest way is to let virt-install create a new disk for you:

--disk size=40,format=qcow2,bus=virtio,pool=default
  • size=40: Creates a 40GB disk.
  • format=qcow2: Uses the QCOW2 format, which is sparse (only uses space it needs) and supports snapshots.
  • bus=virtio: (Recommended) Uses the virtio paravirtualized driver for best performance.
  • pool=default: Stores the disk in the default libvirt storage pool (usually /var/lib/libvirt/images/).

Using Existing Disk Images or LVM Volumes

If you have a pre-created disk image or an LVM logical volume:

--disk path=/var/lib/libvirt/images/existing-disk.qcow2,bus=virtio
--disk path=/dev/vg_data/vm_disk_web1,bus=virtio

When using an LVM volume, ensure it's not currently in use by another VM and has adequate permissions.

Network Configuration

Networking typically involves connecting to a libvirt network.

  • --network network=default: Connects the VM to libvirt's default NAT-based network. This is common for development or isolated VMs.
  • --network network=mynetwork,model=virtio: Connects to a user-defined libvirt network called mynetwork. Specifying model=virtio ensures the paravirtualized network driver is used for performance.
  • --network bridge=br0,model=virtio: Connects the VM directly to a host bridge (e.g., br0). This places the VM directly on the physical network, useful for production environments. Ensure br0 is properly configured on the host.
  • --network type=direct,source=eth0,source_mode=bridge,model=virtio: Similar to a bridge, but uses direct attachment to a physical interface. source_mode=bridge specifies how the interface should be used.

Unattended Installation with Cloud-Init and Cloud Images

The most powerful use case for virt-install is provisioning VMs automatically using cloud-init with pre-built cloud images. This allows for fully unattended deployments.

Preparing Cloud Images

First, download a cloud image. For Ubuntu 22.04, you'd use:

wget https://cloud-images.ubuntu.com/releases/jammy/release/ubuntu-22.04-server-cloudimg-amd64.img -O /var/lib/libvirt/images/ubuntu-22.04-cloud.qcow2
sudo qemu-img resize /var/lib/libvirt/images/ubuntu-22.04-cloud.qcow2 40G # Resize to desired disk size

Note: The cloud image is usually quite small initially. Resizing it with qemu-img resize before VM creation ensures the guest OS sees the full disk size on first boot, and cloud-init can expand the filesystem accordingly.

Using --cloud-init for Automated Configuration

The --cloud-init option (available in virt-install 3.0 and later, part of libvirt 8.0+) generates an ISO containing cloud-init user data and network configuration. This ISO is then attached to the VM.

  • host-name=<NAME>: Sets the hostname.
  • user-data=<FILE>: Path to a file containing cloud-init user data (YAML format). This specifies users, SSH keys, package installs, and custom scripts.
  • network-config=<FILE>: Path to a file containing cloud-init network configuration (YAML format). Defines interfaces, IP addresses, DNS.
  • meta-data=<FILE>: Path to a file containing cloud-init meta-data.

Example user-data.yaml:

#cloud-config
users:
  - name: stackvora
    sudo: ALL=(ALL) NOPASSWD:ALL
    groups: users, admin
    shell: /bin/bash
    ssh_authorized_keys:
      - ssh-rsa AAAAB3NzaC...your-public-key...
package_update: true
package_upgrade: true
packages:
  - htop
  - net-tools
runcmd:
  - echo "VM provisioned successfully!" >> /var/log/cloud-init-output.log

Example network-config.yaml (for a static IP on a bridged network, using netplan syntax):

network:
  version: 2
  ethernets:
    enp1s0:
      dhcp4: no
      addresses:
        - 192.168.1.100/24
      routes:
        - to: default
          via: 192.168.1.1
      nameservers:
          addresses: [8.8.8.8, 8.8.4.4]

Now, combine it all for an unattended VM creation:

sudo virt-install \
    --name web1 \
    --memory 4096 \
    --vcpus 2 \
    --disk path=/var/lib/libvirt/images/ubuntu-22.04-cloud.qcow2,bus=virtio \
    --import \
    --network bridge=br0,model=virtio \
    --os-variant ubuntu22.04 \
    --cloud-init host-name=web1,user-data=./user-data.yaml,network-config=./network-config.yaml \
    --graphics none \
    --console pty,target_type=serial \
    --noautoconsole
  • --disk path=...,bus=virtio: Points to the pre-resized cloud image.
  • --import: Indicates that you're importing an existing disk image, rather than installing from scratch.
  • --cloud-init ...: Generates the cloud-init ISO.
  • --graphics none --console pty,target_type=serial: Sets up a serial console, which is standard for cloud images and allows headless operation.
  • --noautoconsole: Prevents virt-install from immediately connecting to the serial console. You can connect later with virsh console web1.

After running this, the VM web1 will boot, cloud-init will execute the provided scripts, configure networking, and create the specified user. You should then be able to SSH into it using the defined IP address and SSH key.

Advanced virt-install Options and Considerations

CPU Tuning

  • --cpu host-passthrough: Passes through the host CPU features directly to the guest. Often provides the best performance and compatibility.
  • --cpu host-model: Tells QEMU to emulate a CPU model that is very similar to the host CPU.
  • --cpu <MODEL>,socket=<N>,core=<M>,thread=<T>: Finer-grained control over virtual CPU topology.

Graphics and Console

  • --graphics vnc,port=5901,listen=127.0.0.1: VNC console on port 5901, only accessible from the host.
  • --graphics spice,port=5902,listen=0.0.0.0: SPICE console (often preferred for better performance than VNC), accessible from anywhere.
  • --console pty,target_type=serial: Standard serial console, essential for headless cloud-init VMs.

Persistent Domain Definition

By default, virt-install creates a persistent domain definition. To just create a temporary, non-persistent VM for testing:

--transient

Saving and Re-using Commands

It's highly recommended to save your virt-install commands in a shell script. This ensures repeatability and acts as documentation. For instance, to provision identical VMs, just change the --name and relevant cloud-init files.

#!/bin/bash

VM_NAME="web2"
MEMORY_MB=2048
VCPUS=2
DISK_SIZE_GB=30
CLOUD_IMAGE="/var/lib/libvirt/images/ubuntu-22.04-cloud.qcow2"
USER_DATA_FILE="./user-data-web2.yaml"
NETWORK_CONFIG_FILE="./network-config-web2.yaml"
BRIDGE_NAME="br0"

# Ensure the cloud image is ready and resized
if [ ! -f "${CLOUD_IMAGE}" ]; then
    echo "Error: Cloud image not found at ${CLOUD_IMAGE}"
    exit 1
fi
sudo qemu-img resize "${CLOUD_IMAGE}" "${DISK_SIZE_GB}G"

# Create specific cloud-init config for this VM if necessary
# For example, network-config-web2.yaml might have a different IP
cp user-data.yaml "${USER_DATA_FILE}"
sed "s/stackvora/web2admin/g" user-data.yaml > "${USER_DATA_FILE}" # Example modification
cp network-config.yaml "${NETWORK_CONFIG_FILE}"
sed -i "s/192.168.1.100/192.168.1.101/g" "${NETWORK_CONFIG_FILE}" # Example: change IP

echo "Creating VM: ${VM_NAME}..."

sudo virt-install \
    --name "${VM_NAME}" \
    --memory "${MEMORY_MB}" \
    --vcpus "${VCPUS}" \
    --disk path="${CLOUD_IMAGE}",bus=virtio \
    --import \
    --network bridge="${BRIDGE_NAME}",model=virtio \
    --os-variant ubuntu22.04 \
    --cloud-init host-name="${VM_NAME}",user-data="${USER_DATA_FILE}",network-config="${NETWORK_CONFIG_FILE}" \
    --graphics none \
    --console pty,target_type=serial \
    --noautoconsole

if [ $? -eq 0 ]; then
    echo "VM ${VM_NAME} created successfully."
    echo "You can connect via serial console: virsh console ${VM_NAME}"
    echo "Or via SSH to its assigned IP address."
else
    echo "Error creating VM ${VM_NAME}."
fi

Common Pitfalls and Troubleshooting

  • Missing KVM/libvirt: Ensure KVM virtualization is enabled in your host's BIOS/UEFI and that libvirtd service is running (sudo systemctl status libvirtd).
  • Disk Path/Permissions: Verify the disk image path is correct and the libvirt user (typically qemu) has read/write permissions to it and its containing directory (/var/lib/libvirt/images/ is the default and usually has correct permissions).
  • Network Bridge Configuration: If using --network bridge=br0, ensure br0 exists and is correctly configured on the host machine. Use ip a to check.
  • Cloud-Init Issues:
    • Syntax errors in user-data.yaml or network-config.yaml are common. Use a YAML linter.
    • Ensure the cloud image supports cloud-init. Most official cloud images do.
    • Check cloud-init logs inside the VM: /var/log/cloud-init-output.log and /var/log/cloud-init.log.
    • If SSH isn't working, verify your SSH public key in user-data.yaml.
  • VM Not Booting:
    • Check for errors reported by virt-install.
    • Connect to the console (virsh console <VM_NAME> for serial, or virt-viewer <VM_NAME> for graphical) to see boot messages.
    • Ensure --import is used with pre-built disk images.
  • os-variant Mismatch: While not always critical, using an incorrect --os-variant can lead to suboptimal performance or missing guest agent features. Use osinfo-query os to find the correct one.

Back to the knowledge base · Ask the AI assistant