Creating a new vm with libvirt - am I doing this right?

Hi,
New user here, but not a new user of Ansible. I am pretty new to running things elsewhere than on bare metal, and last weekend I created a role to create a Debian virtual machine because I ran out of bare metal to play with :).
However, it does not ‘feel’ right, and my online searches do not result in practical information how to set this up. I am now using a mixture of

  • libvirt to manage the vm
  • ansible commands such as virt-customize to configure the vm
  • netplan via ansible copy to set up the vm’s network

But knowing that cloud-init makes configuration a breeze, I was hoping to use ansible to template the cloud-init configuration and be done with it, but just don’t find that option… Am I overlooking something obvious? Below the most important bits from the role:

- name: Get VMs list
  community.libvirt.virt:
    command: list_vms
  register: existing_vms
  changed_when: no

- name: Create VM if not exists
  when: "vm_name not in existing_vms.list_vms"
  block:
  - name: Download base image
    ansible.builtin.get_url:
      url: "{{ base_image_url }}"
      dest: "/tmp/{{ base_image_name }}"

  - name: Copy base image to libvirt directory
    ansible.builtin.copy:
      src: "/tmp/{{ base_image_name }}"
      dest: "{{ libvirt_pool_dir }}/{{ vm_name }}.qcow2"
      force: no
      remote_src: yes
      mode: 0660
    register: copy_results

  - name: Copy netplan config to /tmp
    ansible.builtin.copy:
      src: netplan_eth_dhcp.config
      dest: /tmp/00-installer.yaml
      force: no
      mode: '660'

  - name: Configure the image (1/3)
    ansible.builtin.command: |
      virt-customize -a {{ libvirt_pool_dir }}/{{ vm_name }}.qcow2 \
      --hostname {{ vm_name }} \
      --root-password password:{{ vm_root_pass }} \
      --ssh-inject 'root:file:{{ ssh_key }}' \
      --install openvswitch-switch-dpdk,console-data,console-setup \
      --copy-in /tmp/00-installer.yaml:/etc/netplan \
      --chmod 600:/etc/netplan/00-installer.yaml \
      --timezone {{ site.tz }} \
      --run-command 'touch /etc/default/keyboard' \
      --edit '/etc/default/keyboard: s/^XKBLAYOUT=.*/XKBLAYOUT="be"/'

  - name: Configure the image (2/3)
    ansible.builtin.command: |
      virt-customize -a {{ libvirt_pool_dir }}/{{ vm_name }}.qcow2 \
      --run-command 'ssh-keygen -A'

  - name: Configure the image (3/3)
    ansible.builtin.command: |
      virt-customize -a {{ libvirt_pool_dir }}/{{ vm_name }}.qcow2 \
      --firstboot-command 'netplan apply'

  - name: Define vm
    community.libvirt.virt:
      command: define
      xml: "{{ lookup('template', 'vm-template.xml.j2') }}"

  - name: Grow qcow2 disk size
    ansible.builtin.command: qemu-img resize {{ libvirt_pool_dir }}/{{ vm_name }}.qcow2 +{{ vm_disk_size_plus }}

- name: Ensure VM is started
  community.libvirt.virt:
    name: "{{ vm_name }}"
    state: running
  register: vm_start_results
  until: "vm_start_results is success"
  retries: 15
  delay: 2

- name: Ensure temporary file is deleted
  ansible.builtin.file:
    path: "/tmp/{{ base_image_name }}"
    state: absent
  when: cleanup_tmp | bool

- name: Ensure VM is restarted (1/2) - workaround bug
  community.libvirt.virt:
    name: "{{ vm_name }}"
    command: destroy
  register: libvirt_status
  until: libvirt_status.status is defined and libvirt_status.status == 'shutdown'
  retries: 20
  delay: 10

- name: Ensure VM is restarted (2/2) - workaround bug
  community.libvirt.virt:
    name: "{{ vm_name }}"
    command: start

Thank you!

Returning to this task after 2 months to find out that the “community.libvirt” collection has received a sizable update to v2.0 and now a virt-install module is added.

I did not succeed in using it with a cloud-init config however. It kept complaining about user-data config not being recognized, and even after trimming down to the bare minimum (setting “user-data.hostname”), the ansible module continued to error out on me.

I was successful in simply calling virt-install as a command in ansible. Much easier and cleaner than first virt-customizing the qcow2 disk and then manually defining the vm (which was not stable anyway)!

- name: Ensure requirements in place for Archlinux host
  ansible.builtin.package:
    name:
      - virt-install
      - guestfs-tools
      - libvirt-python
    state: present
  become: yes

- name: Get VMs list
  community.libvirt.virt:
    command: list_vms
  register: vms
  changed_when: no

- name: Create VM if not exists
  when: "vm_name not in vms.list_vms"
  block:

  - name: Download base image
    ansible.builtin.get_url:
      url: "{{ base_image_url }}"
      dest: "/home/xxx/Virtual Machines/{{ base_image_name }}"
      checksum: "{{ base_image_sha_algo }}:{{ base_image_sha }}"
      force: no

  - name: Copy base image to libvirt directory
    ansible.builtin.copy:
      src: "/home/xxx/Virtual Machines/{{ base_image_name }}"
      dest: "{{ libvirt_pool_dir }}/{{ vm_name }}.qcow2"
      force: no
      remote_src: yes
      mode: 0660
    register: copy_base_image

  - name: Grow qcow2 disk size
    ansible.builtin.command: qemu-img resize {{ libvirt_pool_dir }}/{{ vm_name }}.qcow2 +{{ vm_disk_size_plus }}
    when: copy_base_image is changed

  - name: Generate cloud-init user-data from template
    delegate_to: localhost
    ansible.builtin.template:
      src: vm-user-data.j2
      dest: /tmp/ansible_cloudinit_userdata.config
    when: copy_base_image is changed

  - name: Copy cloud-init network-config from file
    delegate_to: localhost
    ansible.builtin.copy:
      src: netplan_eth_dhcp.config
      dest: /tmp/ansible_cloudinit_network.config
    when: copy_base_image is changed

  - name: Install the vm using an ansible command
    ansible.builtin.command: |
      virt-install
      --name "{{ vm_name }}"
      --boot uefi
      --machine q35
      --memtune hard_limit=4563402
      --memory "{{ vm_ram_mb }}"
      --vcpus "{{ vm_vcpus }}"
      --disk "path={{ libvirt_pool_dir }}/{{ vm_name }}.qcow2"
      --import
      --os-variant "debian12"
      --network "network=default,model=virtio,driver.iommu=on,mac=52:54:00:12:34:56"
      --rng /dev/urandom,driver.iommu=on
      --memballoon driver.iommu=on
      --virt-type kvm
      --cloud-init "user-data=/tmp/ansible_cloudinit_userdata.config,root-ssh-key={{ ssh_key }},network-config=/tmp/ansible_cloudinit_network.config"
      --graphics spice
      --noautoconsole
      --qemu-commandline="-smbios type=1,serial=ds=nocloud;h={{ vm_name }}.{{ site.domain }}"
    when: copy_base_image is changed
    register: libvirt_virtinstall
    failed_when: not 'Installation has exceeded specified time limit' in libvirt_virtinstall.stdout