--- # Provision a KVM/libvirt VM from an Ubuntu cloud image. # Runs on the hypervisor host (localhost) without requiring sudo. # All writes to /var/lib/libvirt/images/ go through libvirtd (virsh vol-upload). - name: Validate required variables ansible.builtin.assert: that: - vm_domain | length > 0 - vm_hostname | length > 0 - vm_disk_path | length > 0 - vm_mac | length > 0 - vm_ip | length > 0 fail_msg: "vm_domain, vm_hostname, vm_disk_path, vm_mac, and vm_ip must all be set in host_vars" # ── Cloud image ──────────────────────────────────────────────────────────── - name: Check if cloud image volume exists in pool ansible.builtin.command: cmd: virsh -c {{ vm_libvirt_uri }} vol-info --pool default {{ vm_cloud_image_cache | basename }} register: cloud_image_vol_stat failed_when: false changed_when: false - name: Download Ubuntu cloud image to temp path ansible.builtin.get_url: url: "{{ vm_cloud_image_url }}" dest: "/tmp/{{ vm_cloud_image_cache | basename }}" mode: "0644" timeout: 600 when: cloud_image_vol_stat.rc != 0 - name: Create cloud image volume in pool ansible.builtin.shell: cmd: > virsh -c {{ vm_libvirt_uri }} vol-create-as default '{{ vm_cloud_image_cache | basename }}' 4G --format raw register: cloud_vol_create failed_when: - cloud_vol_create.rc != 0 - "'exists already' not in cloud_vol_create.stderr" changed_when: cloud_vol_create.rc == 0 when: cloud_image_vol_stat.rc != 0 - name: Upload cloud image to pool ansible.builtin.shell: cmd: > virsh -c {{ vm_libvirt_uri }} vol-upload --pool default '{{ vm_cloud_image_cache | basename }}' '/tmp/{{ vm_cloud_image_cache | basename }}' when: cloud_image_vol_stat.rc != 0 and (cloud_vol_create.rc | default(1)) == 0 - name: Remove temp cloud image download ansible.builtin.file: path: "/tmp/{{ vm_cloud_image_cache | basename }}" state: absent when: cloud_image_vol_stat.rc != 0 # ── Disk image ───────────────────────────────────────────────────────────── - name: Check if VM disk volume exists in pool ansible.builtin.shell: cmd: virsh -c {{ vm_libvirt_uri }} vol-info --pool default '{{ vm_disk_path | basename }}' register: vm_disk_vol_stat failed_when: false changed_when: false - name: Create VM disk volume backed by cloud image ansible.builtin.shell: cmd: > virsh -c {{ vm_libvirt_uri }} vol-create-as default '{{ vm_disk_path | basename }}' {{ vm_disk_size }} --format qcow2 --backing-vol '{{ vm_cloud_image_cache | basename }}' --backing-vol-format qcow2 register: disk_vol_create failed_when: - disk_vol_create.rc != 0 - "'exists already' not in disk_vol_create.stderr" changed_when: disk_vol_create.rc == 0 when: vm_disk_vol_stat.rc != 0 # ── Cloud-init seed ISO ──────────────────────────────────────────────────── - name: Create cloud-init temp directory ansible.builtin.tempfile: state: directory suffix: cloud-init register: cloud_init_dir - name: Write cloud-init user-data ansible.builtin.template: src: cloud-init-user-data.j2 dest: "{{ cloud_init_dir.path }}/user-data" mode: "0644" vars: vm_ssh_keys: "{{ openclaw_ssh_keys | default([]) }}" - name: Write cloud-init meta-data ansible.builtin.template: src: cloud-init-meta-data.j2 dest: "{{ cloud_init_dir.path }}/meta-data" mode: "0644" - name: Set seed ISO facts ansible.builtin.set_fact: vm_seed_iso_name: "{{ vm_hostname }}-seed.iso" vm_seed_iso_tmp: "/tmp/{{ vm_hostname }}-seed.iso" vm_seed_iso: "/var/lib/libvirt/images/{{ vm_hostname }}-seed.iso" - name: Create cloud-init seed ISO in temp path ansible.builtin.command: cmd: > genisoimage -output {{ vm_seed_iso_tmp }} -volid cidata -joliet -rock {{ cloud_init_dir.path }}/user-data {{ cloud_init_dir.path }}/meta-data changed_when: true - name: Create seed ISO volume in pool ansible.builtin.shell: cmd: > virsh -c {{ vm_libvirt_uri }} vol-create-as default '{{ vm_seed_iso_name }}' 4M --format raw register: seed_vol_create failed_when: - seed_vol_create.rc != 0 - "'exists already' not in seed_vol_create.stderr" changed_when: seed_vol_create.rc == 0 - name: Upload seed ISO to pool ansible.builtin.shell: cmd: > virsh -c {{ vm_libvirt_uri }} vol-upload --pool default '{{ vm_seed_iso_name }}' '{{ vm_seed_iso_tmp }}' changed_when: true - name: Clean up cloud-init temp files ansible.builtin.file: path: "{{ item }}" state: absent loop: - "{{ cloud_init_dir.path }}" - "{{ vm_seed_iso_tmp }}" # ── VM definition ────────────────────────────────────────────────────────── - name: Check if VM domain already exists community.libvirt.virt: command: list_vms uri: "{{ vm_libvirt_uri }}" register: existing_vms - name: Define VM from XML template community.libvirt.virt: command: define xml: "{{ lookup('template', 'domain.xml.j2') }}" uri: "{{ vm_libvirt_uri }}" when: vm_domain not in existing_vms.list_vms # ── Network ──────────────────────────────────────────────────────────────── - name: Add static DHCP reservation ansible.builtin.command: cmd: > virsh -c {{ vm_libvirt_uri }} net-update {{ vm_network }} add ip-dhcp-host '' --live --config register: dhcp_result failed_when: - dhcp_result.rc != 0 - "'existing dhcp host entry' not in dhcp_result.stderr" - "'already exists' not in dhcp_result.stderr" changed_when: dhcp_result.rc == 0 # ── Autostart & boot ─────────────────────────────────────────────────────── - name: Enable autostart community.libvirt.virt: name: "{{ vm_domain }}" autostart: true uri: "{{ vm_libvirt_uri }}" - name: Start VM community.libvirt.virt: name: "{{ vm_domain }}" state: running uri: "{{ vm_libvirt_uri }}" - name: Wait for SSH to become available ansible.builtin.wait_for: host: "{{ vm_ip }}" port: 22 delay: 10 timeout: 300 state: started delegate_to: localhost - name: VM is ready ansible.builtin.debug: msg: "VM '{{ vm_domain }}' is up at {{ vm_ip }}. Run install.yml + customize.yml to provision the guest."