--- # Create VMs that do not exist yet. Idempotent by existence check only — this # role does NOT reconcile the config of a live VM (see defaults for why). - name: Check which VMs already exist ansible.builtin.command: cmd: "qm status {{ item.vmid }}" loop: "{{ pve_vms }}" loop_control: label: "{{ item.vmid }} {{ item.name }}" register: _vm_exists changed_when: false failed_when: false check_mode: false run_once: true delegate_to: "{{ pve_vm_node }}" - name: Render cloud-init user-data for VMs that use it # Written to the NFS `snippets` dir so every node can read it. Rendered even # for existing VMs so the file stays in sync with the definition — but see the # template header: cloud-init only APPLIES it on first boot. ansible.builtin.template: src: cloudinit-user.yml.j2 dest: "{{ pve_vm_snippet_path }}/{{ item.name }}-user.yml" mode: "0644" loop: "{{ pve_vms | selectattr('cloudinit', 'defined') | selectattr('cloudinit') | list }}" loop_control: label: "{{ item.name }}" vars: vm: "{{ item }}" run_once: true delegate_to: "{{ pve_vm_node }}" - name: Create missing VMs ansible.builtin.command: cmd: >- qm create {{ item.item.vmid }} --name {{ item.item.name }} --description '{{ item.item.description }}' --machine {{ item.item.machine | default('q35') }} --bios {{ item.item.bios | default('ovmf') }} --memory {{ item.item.memory }} --cores {{ item.item.cores }} --cpu {{ item.item.cpu | default('x86-64-v2-AES') }} --scsihw virtio-scsi-single --scsi0 {{ item.item.disk }},discard=on,ssd=1 --efidisk0 {{ item.item.disk.split(':')[0] }}:1,efitype=4m,pre-enrolled-keys=0 --vga {{ item.item.vga | default('virtio') }} {% if item.item.serial | default(false) %}--serial0 socket{% endif %} {% for n in item.item.nets %}--net{{ n.id }} virtio{% if n.mac is defined %}={{ n.mac }}{% endif %},bridge={{ n.bridge }} {% endfor %} {% if item.item.iso is defined %}--ide2 {{ item.item.iso }},media=cdrom --boot order=ide2;scsi0{% else %}--boot order=scsi0{% endif %} {% if item.item.cloudinit | default(false) %}--ide2 {{ item.item.disk.split(':')[0] }}:cloudinit --ipconfig0 ip=dhcp --ciuser {{ item.item.ciuser }}{% endif %} --agent enabled=1 --onboot {{ item.item.onboot | default(0) }} --ostype l26 loop: "{{ _vm_exists.results }}" loop_control: label: "{{ item.item.vmid }} {{ item.item.name }}" when: item.rc != 0 run_once: true delegate_to: "{{ pve_vm_node }}" - name: Point those VMs at their user-data ansible.builtin.command: cmd: >- qm set {{ item.vmid }} --cicustom user={{ pve_vm_snippet_storage }}:snippets/{{ item.name }}-user.yml loop: "{{ pve_vms | selectattr('cloudinit', 'defined') | selectattr('cloudinit') | list }}" loop_control: label: "{{ item.name }}" register: _cicustom changed_when: "'update VM' in (_cicustom.stdout | default(''))" run_once: true delegate_to: "{{ pve_vm_node }}" - name: Write base images onto newly created disks ansible.builtin.include_tasks: image.yml # ALL image-backed VMs, not just newly created ones: image.yml decides by # checking for an MBR on the disk. Gating on "was just created" meant a failed # image write could never be repaired by re-running. loop: "{{ pve_vms | selectattr('image', 'defined') | list }}" loop_control: label: "{{ item.name }}" run_once: true - name: Report ansible.builtin.debug: msg: >- {{ _vm_exists.results | map(attribute='item') | zip(_vm_exists.results | map(attribute='rc')) | map('join', ' rc=') | list }} run_once: true