Simplify Linux VM installation on KVM/QEMU with virt-install and cloud-init

This is a follow-up post of my previous post about Windows VM installation. This one, surprise surprise, is about installing Linux VM.

I hate tedious and manual work, but sometimes it also doesn't make sense to spend time modifying an Ansible playbook that I probably will only use a few times. I find virt-install and cloud-init meet most of my needs when it comes to quickly spinning VMs up for testing. They offer simplicity with great flexibility. Within minutes I can create VMs for testing; if I want to go crazy, I can tell it to run Ansible during the first boot. Probably other automation tools as well.

For more serious stuff (like a production server), I will stick to Ansible for deployment and config management.


I will use Ubuntu as example for this tutorial. Debian/Fedora/CentOS Stream all have cloud editions. Download cloud image .img file

In a directory, create files meta-data and user-data (optionally vendor-data)

meta-data:

instance-id: <Your-ID> # not important; will not be in virtual machine's XML file
local-hostname: ubuntu.local.lan # this will be the FQDN

user-data docs and examples

users:
  - name: user1
    gecos: A super admin user on Ubuntu with nopassword sudo; 
    groups: [sudo, adm, audio, cdrom, dialout, floppy, video, plugdev, dip, netdev] 
    # other than sudo, the rest are ubuntu defaults
    shell: /bin/bash
    sudo: 'ALL=(ALL) NOPASSWD:ALL'
    lock_passwd: true # by default; disables password login
    chpasswd:
      expire: True
    ssh_authorized_keys:
      - <Your SSH pub key>

    # Another example
  - name: user2
    gecos: A generic admin user with sudo privilege but requires password
    groups: users,admin,wheel
    shell: /bin/bash
    sudo: 'ALL=(ALL) ALL'
    passwd: <hash of password> # mkpasswd --method=SHA-512 --rounds=4096 ## to get the hash
    ssh_authorized_keys:
      - ' <Your SSH pub key>'

package_update: true
package_upgrade: true # default command on Ubuntu is 'apt dist-upgrade'

# installing additional packages
packages:
  - ansible

# cloud-init is able to chain Ansible pull mode, if further configuration is needed
ansible:
  pull:
    url: "https://git.../xxx.git"
    playbook_name: xxx.yml

# run some commands on first boot
bootcmd: # very similar to runcmd, but commands run very early in the boot process, only slightly after a 'boothook' would run.
- some commands...
runcmd:
- systemctl daemon-reload

#swap: # by default, there is no swap
#  filename: /swap
#  size: "auto" # or size in bytes
#  maxsize: 2147484000   # size in bytes (2 Gibibyte)

# after system comes up first time; find IP in the output text
final_message: "The system is finally up, after $UPTIME seconds"

Finally, install the VM with cloud-init scripts and the cloud image we downloaded earlier. We are going to use user session qemu:///session and store the qcow2 image to ~/.local/share/libvirt/images/xxx.qcow2

virt-install \
  --connect qemu:///session \
  --name ubuntu \
  --vcpus 2 \ # --cpu MODEL[,+feature][,-feature][,match=MATCH][,vendor=VENDOR],...
  --memory 2048 \
  #--memballoon driver.iommu=on \
  --osinfo ubuntu22.04 \
  --network bridge=virbr0,model=virtio,driver.iommu=on \
  --graphics none \ # server install
  --disk ~/.local/share/libvirt/images/xxx.qcow2,size=30,backing_store=$PWD"/jammy-server-cloudimg-amd64.img",target.bus=virtio \
  --cloud-init user-data=$PWD"/user-data",meta-data=${PWD}"/meta-data"
  # to get the list of accepted OS variant `virt-install --osinfo list` debian11/fedora37/win10;

As usual, tweak any flags as you see fit.