--- # Provision a KVM/libvirt VM from an Ubuntu cloud image. # Runs on the hypervisor host (localhost). - 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" - name: Install host dependencies ansible.builtin.package: name: - qemu-img - genisoimage - libvirt-utils state: present # ── Cloud image ──────────────────────────────────────────────────────────── - name: Check if cloud image cache exists ansible.builtin.stat: path: "{{ vm_cloud_image_cache }}" register: cloud_image_stat - name: Download Ubuntu cloud image ansible.builtin.get_url: url: "{{ vm_cloud_image_url }}" dest: "{{ vm_cloud_image_cache }}" mode: "0644" timeout: 300 when: not cloud_image_stat.stat.exists # ── Disk image ───────────────────────────────────────────────────────────── - name: Check if VM disk already exists ansible.builtin.stat: path: "{{ vm_disk_path }}" register: vm_disk_stat - name: Create VM disk from cloud image ansible.builtin.command: cmd: > qemu-img create -f qcow2 -F qcow2 -b {{ vm_cloud_image_cache }} {{ vm_disk_path }} {{ vm_disk_size }} creates: "{{ vm_disk_path }}" when: not vm_disk_stat.stat.exists # ── 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 path fact ansible.builtin.set_fact: vm_seed_iso: "/var/lib/libvirt/images/{{ vm_hostname }}-seed.iso" - name: Create cloud-init seed ISO ansible.builtin.command: cmd: > genisoimage -output {{ vm_seed_iso }} -volid cidata -joliet -rock {{ cloud_init_dir.path }}/user-data {{ cloud_init_dir.path }}/meta-data changed_when: true - name: Clean up cloud-init temp directory ansible.builtin.file: path: "{{ cloud_init_dir.path }}" state: absent # ── 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 - "'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: 180 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."