feat: Implement Debian VM template creation and cloning on Proxmox

- Added default configuration for VM creation in defaults/main.yml.
- Created tasks for configuring the VM with UEFI, TPM, disks, GPU, and Cloud-Init in tasks/configure-vm.yml.
- Implemented clone creation and configuration logic in tasks/create-clones.yml.
- Added template conversion functionality in tasks/create-template.yml.
- Developed base VM creation logic in tasks/create-vm.yml.
- Included image download and caching tasks in tasks/download-image.yml.
- Introduced utility tasks for common operations in tasks/helpers.yml.
- Organized main orchestration logic in tasks/main.yml, with clear stages for each operation.
- Added pre-flight checks to validate the environment before execution in tasks/preflight-checks.yml.
This commit is contained in:
2025-11-15 17:22:21 +01:00
parent f76fb397ab
commit f62750fe2f
20 changed files with 4223 additions and 177 deletions

169
tasks/configure-vm.yml Normal file
View File

@@ -0,0 +1,169 @@
---
# configure-vm.yml - Configure VM with UEFI, TPM, disks, GPU, and Cloud-Init
- name: "[CONFIG] Configure UEFI + Secure Boot + TPM (if enabled)"
block:
- name: "[CONFIG] Enable UEFI and TPM"
command: >
qm set {{ vm_id }}
--bios ovmf
--efidisk0 {{ storage }}:0,pre-enrolled-keys=1
--tpmstate0 {{ storage }}:1,size=4M,version=v2.0
register: tpm_config
changed_when: tpm_config.rc == 0
- name: "[CONFIG] Verify TPM configuration"
debug:
msg: "✓ UEFI + TPM configured for VM {{ vm_id }}"
when: enable_tpm | default(false)
- name: "[CONFIG] Import and attach disk"
block:
- name: "[CONFIG] Check if disk already exists"
stat:
path: "/var/lib/vz/images/{{ vm_id }}/vm-{{ vm_id }}-disk-0.qcow2"
register: disk_exists
changed_when: false
- name: "[CONFIG] Import qcow2 disk"
command: >
qm importdisk {{ vm_id }}
{{ debian_image_path }}
{{ storage }}
register: disk_import
retries: 3
delay: 5
until: disk_import is succeeded
when: not disk_exists.stat.exists
- name: "[CONFIG] Verify disk import"
fail:
msg: "Disk import failed for VM {{ vm_id }}"
when:
- not disk_exists.stat.exists
- disk_import is failed
- name: "[CONFIG] Attach imported disk"
command: >
qm set {{ vm_id }}
--scsihw virtio-scsi-pci
--scsi0 {{ storage }}:vm-{{ vm_id }}-disk-0
register: disk_attach
when: not disk_exists.stat.exists
changed_when: disk_attach.rc == 0
- name: "[CONFIG] Enable serial console and set boot order"
command: >
qm set {{ vm_id }}
--serial0 socket
--boot order=scsi0
register: serial_config
changed_when: serial_config.rc == 0
- name: "[CONFIG] Display disk configuration"
debug:
msg: "✓ Disk configured and attached to VM {{ vm_id }}"
rescue:
- name: "[CONFIG] Handle disk configuration error"
fail:
msg: |
Failed to configure disk for VM {{ vm_id }}:
{{ ansible_failed_result | default('Unknown error') }}
- name: "[CONFIG] Resize disk (if enabled)"
block:
- name: "[CONFIG] Resize disk"
command: "qm resize {{ vm_id }} scsi0 {{ resize_size }}"
register: disk_resize
changed_when: disk_resize.rc == 0
- name: "[CONFIG] Display disk resize result"
debug:
msg: "✓ Disk resized to {{ resize_size }}"
when: resize_disk | default(false)
- name: "[CONFIG] Configure GPU passthrough (if enabled)"
block:
- name: "[CONFIG] Enable PCI GPU passthrough"
command: "qm set {{ vm_id }} --hostpci0 {{ gpu_device }}"
register: gpu_config
changed_when: gpu_config.rc == 0
- name: "[CONFIG] Display GPU configuration"
debug:
msg: "✓ GPU passthrough configured: {{ gpu_device }}"
when: gpu_passthrough | default(false)
- name: "[CONFIG] Configure VirtIO GPU (if enabled)"
block:
- name: "[CONFIG] Enable VirtIO GPU"
command: "qm set {{ vm_id }} --vga virtio"
register: virtio_gpu_config
changed_when: virtio_gpu_config.rc == 0
- name: "[CONFIG] Display VirtIO GPU configuration"
debug:
msg: "✓ VirtIO GPU configured"
when: virtio_gpu | default(false)
- name: "[CONFIG] Create and apply Cloud-Init snippets"
block:
- name: "[CONFIG] Create Cloud-Init vendor-data snippet"
template:
src: cloudinit_vendor.yaml.j2
dest: "/var/lib/vz/snippets/{{ vm_id }}-vendor.yaml"
mode: "0644"
register: vendor_snippet
- name: "[CONFIG] Create Cloud-Init user-data snippet"
template:
src: cloudinit_userdata.yaml.j2
dest: "/var/lib/vz/snippets/{{ vm_id }}-user.yaml"
mode: "0644"
register: user_snippet
- name: "[CONFIG] Verify SSH key is readable"
stat:
path: "{{ ssh_key_path | expanduser }}"
register: ssh_key_stat
failed_when: not ssh_key_stat.stat.readable
- name: "[CONFIG] Copy SSH public key to snippets"
copy:
src: "{{ ssh_key_path | expanduser }}"
dest: "/var/lib/vz/snippets/{{ vm_id }}-sshkey.pub"
mode: "0644"
register: ssh_snippet
- name: "[CONFIG] Apply Cloud-Init configuration"
command: >
qm set {{ vm_id }}
--ciuser {{ ci_user }}
--sshkeys local:snippets/{{ vm_id }}-sshkey.pub
--hostname {{ hostname }}
--citype nocloud
--cicustom "user=local:snippets/{{ vm_id }}-user.yaml,vendor=local:snippets/{{ vm_id }}-vendor.yaml"
--ipconfig0 {{ ipconfig0 }}
register: cloudinit_apply
changed_when: cloudinit_apply.rc == 0
- name: "[CONFIG] Display Cloud-Init configuration"
debug:
msg: |
✓ Cloud-Init configured
- User: {{ ci_user }}
- Hostname: {{ hostname }}
- IP Config: {{ ipconfig0 }}
- Timezone: {{ timezone }}
rescue:
- name: "[CONFIG] Handle Cloud-Init configuration error"
fail:
msg: |
Failed to configure Cloud-Init for VM {{ vm_id }}:
{{ ansible_failed_result | default('Unknown error') }}

102
tasks/create-clones.yml Normal file
View File

@@ -0,0 +1,102 @@
---
# create-clones.yml - Create and configure clones from template with error handling
- name: "[CLONES] Validate clone list is not empty"
fail:
msg: "No clones defined in 'clones' variable"
when:
- create_clones | default(false)
- clones is not defined or clones | length == 0
- name: "[CLONES] Process each clone"
block:
- name: "[CLONES] Check if clone already exists"
stat:
path: "/etc/pve/qemu-server/{{ clone.id }}.conf"
register: clone_conf
changed_when: false
- name: "[CLONES] Display clone status"
debug:
msg: "Clone {{ clone.id }} ({{ clone.hostname }}) - Status: {{ 'EXISTS' if clone_conf.stat.exists else 'WILL BE CREATED' }}"
- name: "[CLONES] Clone VM from template"
block:
- name: "[CLONES] Execute clone command"
command: >
qm clone {{ vm_id }} {{ clone.id }}
--name {{ clone.hostname }}
--full {{ clone.full | default(0) }}
register: clone_cmd
when: not clone_conf.stat.exists
- name: "[CLONES] Verify clone was created"
stat:
path: "/etc/pve/qemu-server/{{ clone.id }}.conf"
register: clone_verify
changed_when: false
failed_when: not clone_verify.stat.exists
- name: "[CLONES] Wait for clone to be ready"
pause:
seconds: 2
when: not clone_conf.stat.exists
rescue:
- name: "[CLONES] Handle clone creation error"
fail:
msg: |
Failed to clone VM {{ vm_id }} to {{ clone.id }}:
{{ ansible_failed_result | default('Unknown error') }}
- name: "[CLONES] Configure Cloud-Init for clone (if needed)"
block:
- name: "[CLONES] Set clone hostname and IP"
command: >
qm set {{ clone.id }}
--hostname {{ clone.hostname }}
--ipconfig0 "ip={{ clone.ip }},gw={{ clone.gateway }}"
register: clone_config
when: not clone_conf.stat.exists
- name: "[CLONES] Apply SSH keys to clone"
command: >
qm set {{ clone.id }}
--sshkeys local:snippets/{{ vm_id }}-sshkey.pub
when: not clone_conf.stat.exists
rescue:
- name: "[CLONES] Handle clone configuration error"
debug:
msg: "WARNING: Could not fully configure clone {{ clone.id }}. You may need to configure manually."
- name: "[CLONES] Start clone VM"
command: "qm start {{ clone.id }}"
register: clone_start
retries: 3
delay: 2
until: clone_start is succeeded
when: not clone_conf.stat.exists
- name: "[CLONES] Wait for clone to boot"
pause:
seconds: 3
- name: "[CLONES] Display clone creation result"
debug:
msg: |
✓ Clone created and started
- ID: {{ clone.id }}
- Hostname: {{ clone.hostname }}
- IP: {{ clone.ip }}
- Full clone: {{ clone.full | default(0) }}
loop: "{{ clones }}"
loop_control:
loop_var: clone
when: create_clones | default(false)
- name: "[CLONES] Skip clone creation (disabled)"
debug:
msg: " Clone creation is disabled. Set 'create_clones: true' to enable."
when: not (create_clones | default(false))

67
tasks/create-template.yml Normal file
View File

@@ -0,0 +1,67 @@
---
# create-template.yml - Convert VM to template with proper idempotency
- name: "[TEMPLATE] Check if VM is already a template"
shell: "qm config {{ vm_id }} | grep -q 'template: 1'"
register: is_template
changed_when: false
failed_when: false
- name: "[TEMPLATE] Display template status"
debug:
msg: "Template status for VM {{ vm_id }}: {{ 'ALREADY A TEMPLATE' if is_template.rc == 0 else 'NOT YET A TEMPLATE' }}"
- name: "[TEMPLATE] Verify VM is stopped before converting"
block:
- name: "[TEMPLATE] Check VM status"
shell: "qm status {{ vm_id }} | grep -q 'stopped'"
register: vm_stopped
changed_when: false
failed_when: false
- name: "[TEMPLATE] Stop VM if running"
command: "qm stop {{ vm_id }}"
when: vm_stopped.rc != 0
register: vm_stop
- name: "[TEMPLATE] Wait for VM to stop"
pause:
seconds: 2
when: vm_stopped.rc != 0
rescue:
- name: "[TEMPLATE] Handle VM stop error"
debug:
msg: "WARNING: Could not verify/stop VM {{ vm_id }}. Continuing..."
- name: "[TEMPLATE] Convert VM to template"
block:
- name: "[TEMPLATE] Convert to template"
command: "qm template {{ vm_id }}"
register: template_convert
when: is_template.rc != 0
changed_when: template_convert.rc == 0
- name: "[TEMPLATE] Verify conversion"
shell: "qm config {{ vm_id }} | grep 'template: 1'"
register: template_verify
changed_when: false
failed_when: template_verify.rc != 0
- name: "[TEMPLATE] Display template conversion result"
debug:
msg: |
✓ VM {{ vm_id }} ({{ hostname }}) successfully converted to template
Template can now be cloned
rescue:
- name: "[TEMPLATE] Handle template conversion error"
fail:
msg: |
Failed to convert VM {{ vm_id }} to template:
{{ ansible_failed_result | default('Unknown error') }}
- name: "[TEMPLATE] Skip template conversion (already done)"
debug:
msg: " VM {{ vm_id }} is already a template, skipping conversion"
when: is_template.rc == 0

46
tasks/create-vm.yml Normal file
View File

@@ -0,0 +1,46 @@
---
# create-vm.yml - Create base VM on Proxmox
- name: "[VM] Check if VM already exists"
stat:
path: "/etc/pve/qemu-server/{{ vm_id }}.conf"
register: vm_conf
changed_when: false
- name: "[VM] Display VM status"
debug:
msg: "VM {{ vm_id }} ({{ hostname }}) - Status: {{ 'ALREADY EXISTS' if vm_conf.stat.exists else 'WILL BE CREATED' }}"
- name: "[VM] Create base VM"
command: >
qm create {{ vm_id }}
--name {{ hostname }}
--memory {{ memory }}
--cores {{ cores }}
--cpu {{ cpu_type }}
--net0 virtio,bridge={{ bridge }},macaddr={{ mac_address }}
--agent 1
register: vm_create
when: not vm_conf.stat.exists
changed_when: vm_create.rc == 0
- name: "[VM] Handle VM creation error"
fail:
msg: |
Failed to create VM {{ vm_id }}:
{{ vm_create.stderr | default('No error message') }}
when:
- not vm_conf.stat.exists
- vm_create is failed
- name: "[VM] Verify VM was created"
stat:
path: "/etc/pve/qemu-server/{{ vm_id }}.conf"
register: vm_conf_verify
changed_when: false
failed_when: not vm_conf_verify.stat.exists
- name: "[VM] Display VM creation result"
debug:
msg: "✓ VM {{ vm_id }} created successfully"
when: not vm_conf.stat.exists

41
tasks/download-image.yml Normal file
View File

@@ -0,0 +1,41 @@
---
# download-image.yml - Download and cache Debian GenericCloud image
- name: "[IMAGE] Check for Debian GenericCloud image"
stat:
path: "{{ debian_image_path }}"
register: debian_img
changed_when: false
- name: "[IMAGE] Create template directory if missing"
file:
path: "/var/lib/vz/template/qemu"
state: directory
mode: "0755"
when: not debian_img.stat.exists
- name: "[IMAGE] Download Debian GenericCloud qcow2"
get_url:
url: "{{ debian_image_url }}"
dest: "{{ debian_image_path }}"
mode: "0644"
timeout: 300
register: image_download
retries: 3
delay: 5
until: image_download is succeeded
when: not debian_img.stat.exists
- name: "[IMAGE] Verify downloaded image integrity"
stat:
path: "{{ debian_image_path }}"
register: debian_img_final
changed_when: false
failed_when: not debian_img_final.stat.exists or debian_img_final.stat.size == 0
- name: "[IMAGE] Display image info"
debug:
msg: |
Image cached at: {{ debian_image_path }}
Size: {{ debian_img_final.stat.size | int / 1024 / 1024 / 1024 | round(2) }} GB
Last modified: {{ debian_img_final.stat.mtime | timestamp_to_datetime }}

149
tasks/helpers.yml Normal file
View File

@@ -0,0 +1,149 @@
---
# helpers.yml - Utility tasks for common operations
# Usage:
# - name: Check if VM exists
# include_tasks: helpers.yml
# vars:
# helper_task: check_vm_exists
# target_vm_id: "{{ vm_id }}"
##################################################################
# CHECK VM EXISTS
##################################################################
- name: "[HELPER] Check VM exists"
block:
- name: "[HELPER] Stat VM config file"
stat:
path: "/etc/pve/qemu-server/{{ target_vm_id }}.conf"
register: vm_config
changed_when: false
- name: "[HELPER] Set fact: vm_exists"
set_fact:
vm_exists: "{{ vm_config.stat.exists }}"
when: helper_task == "check_vm_exists"
##################################################################
# CHECK IF VM IS TEMPLATE
##################################################################
- name: "[HELPER] Check if VM is template"
block:
- name: "[HELPER] Query VM template status"
shell: "qm config {{ target_vm_id }} | grep -q '^template: 1$'"
changed_when: false
failed_when: false
register: template_check
- name: "[HELPER] Set fact: is_template"
set_fact:
is_template: "{{ template_check.rc == 0 }}"
when: helper_task == "check_template"
##################################################################
# CHECK VM STATUS
##################################################################
- name: "[HELPER] Check VM running status"
block:
- name: "[HELPER] Query VM status"
shell: "qm status {{ target_vm_id }} | grep -oP 'status: \\K\\w+'"
changed_when: false
register: vm_status_cmd
- name: "[HELPER] Set fact: vm_status"
set_fact:
vm_status: "{{ vm_status_cmd.stdout | default('unknown') }}"
when: helper_task == "check_vm_status"
##################################################################
# CHECK STORAGE AVAILABLE
##################################################################
- name: "[HELPER] Check storage space"
block:
- name: "[HELPER] Query storage status"
command: "pvesm status {{ storage_name }}"
changed_when: false
register: storage_status
- name: "[HELPER] Extract available space"
set_fact:
storage_available: "{{ storage_status.stdout_lines[1].split()[1] | int }}"
when: helper_task == "check_storage"
##################################################################
# VALIDATE VM ID
##################################################################
- name: "[HELPER] Validate VM ID"
block:
- name: "[HELPER] Check VM ID format"
assert:
that:
- target_vm_id | int >= 100
- target_vm_id | int <= 999999
fail_msg: "Invalid VM ID {{ target_vm_id }}. Must be between 100 and 999999"
- name: "[HELPER] Check if ID already in use"
stat:
path: "/etc/pve/qemu-server/{{ target_vm_id }}.conf"
register: id_check
changed_when: false
- name: "[HELPER] Warn if ID exists"
debug:
msg: "WARNING: VM ID {{ target_vm_id }} already exists"
when: id_check.stat.exists
when: helper_task == "validate_vm_id"
##################################################################
# GET VM INFO
##################################################################
- name: "[HELPER] Get VM information"
block:
- name: "[HELPER] Read VM config"
slurp:
src: "/etc/pve/qemu-server/{{ target_vm_id }}.conf"
register: vm_config_file
changed_when: false
- name: "[HELPER] Parse VM config"
set_fact:
vm_info: "{{ vm_config_file.content | b64decode }}"
when: helper_task == "get_vm_info"
##################################################################
# LIST ALL VMS
##################################################################
- name: "[HELPER] List all VMs"
block:
- name: "[HELPER] Get VM list"
command: "qm list"
changed_when: false
register: vm_list_output
- name: "[HELPER] Parse VM list"
set_fact:
vm_list: "{{ vm_list_output.stdout_lines[1:] }}"
when: helper_task == "list_vms"
##################################################################
# CLEANUP SNIPPETS
##################################################################
- name: "[HELPER] Cleanup Cloud-Init snippets"
block:
- name: "[HELPER] Remove old snippets for VM"
file:
path: "{{ item }}"
state: absent
loop:
- "/var/lib/vz/snippets/{{ target_vm_id }}-user.yaml"
- "/var/lib/vz/snippets/{{ target_vm_id }}-vendor.yaml"
- "/var/lib/vz/snippets/{{ target_vm_id }}-sshkey.pub"
when: helper_task == "cleanup_snippets"

View File

@@ -1,167 +1,95 @@
---
- name: "Create a Debian VM template and optionally deploy clones"
# main.yml - Orchestrate Debian VM template creation and cloning on Proxmox
# This playbook handles:
# 1. Pre-flight checks (environment validation)
# 2. Image download & caching
# 3. VM creation & configuration
# 4. Template conversion
# 5. Clone creation & deployment
- name: "Create Debian VM template and deploy clones on Proxmox"
hosts: localhost
become: true
gather_facts: false
pre_tasks:
- name: "Display playbook banner"
debug:
msg: |
╔════════════════════════════════════════════════════════════╗
║ Proxmox VM Template & Clone Manager ║
║ Template VM: {{ hostname }} (ID: {{ vm_id }}) ║
║ Storage: {{ storage }} ║
║ CPU: {{ cores }} cores | RAM: {{ memory }}MB ║
╚════════════════════════════════════════════════════════════╝
tasks:
##################################################################
# 1. PREFLIGHT CHECKS
##################################################################
- name: "STAGE 1: Run pre-flight environment checks"
include_tasks: preflight-checks.yml
tags: [preflight, always]
##################################################################
# 1. Ensure Debian GenericCloud Image Exists
# 2. DOWNLOAD IMAGE
##################################################################
- name: Check for Debian image
stat:
path: "/var/lib/vz/template/qemu/debian-genericcloud-amd64.qcow2"
register: debian_img
- name: Download GenericCloud qcow2
get_url:
url: "https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-genericcloud-amd64.qcow2"
dest: "/var/lib/vz/template/qemu/debian-genericcloud-amd64.qcow2"
mode: "0644"
when: not debian_img.stat.exists
- name: "STAGE 2: Download and cache Debian GenericCloud image"
include_tasks: download-image.yml
tags: [image, always]
##################################################################
# 2. Create Base VM (if not exists)
# 3. CREATE VM
##################################################################
- name: Check if VM exists
stat:
path: "/etc/pve/qemu-server/{{ vm_id }}.conf"
register: vm_conf
- name: Create VM
command: >
qm create {{ vm_id }}
--name {{ hostname }}
--memory {{ memory }}
--cores {{ cores }}
--cpu {{ cpu_type }}
--net0 virtio,bridge={{ bridge }},macaddr={{ mac_address }}
--agent 1
when: not vm_conf.stat.exists
- name: "STAGE 3: Create base VM"
include_tasks: create-vm.yml
tags: [vm, create]
##################################################################
# 3. Optional UEFI + Secure Boot + TPM
# 4. CONFIGURE VM (Disk, Cloud-Init, GPU, TPM, etc.)
##################################################################
- name: Enable UEFI + TPM
command: >
qm set {{ vm_id }}
--bios ovmf
--efidisk0 {{ storage }}:0,pre-enrolled-keys=1
--tpmstate0 {{ storage }}:1,size=4M,version=v2.0
when: enable_tpm | default(false)
- name: "STAGE 4: Configure VM (disk, Cloud-Init, optional features)"
include_tasks: configure-vm.yml
tags: [vm, configure, cloudinit]
##################################################################
# 4. Disk Import & Attach
# 5. CREATE TEMPLATE
##################################################################
- name: Check if disk already exists
stat:
path: "/var/lib/vz/images/{{ vm_id }}/vm-{{ vm_id }}-disk-0.qcow2"
register: disk_exists
- name: Import qcow2 disk
command: >
qm importdisk {{ vm_id }}
/var/lib/vz/template/qemu/debian-genericcloud-amd64.qcow2
{{ storage }}
when: not disk_exists.stat.exists
- name: Attach imported disk
command: >
qm set {{ vm_id }}
--scsihw virtio-scsi-pci
--scsi0 {{ storage }}:vm-{{ vm_id }}-disk-0
when: not disk_exists.stat.exists
- name: Enable serial console + boot disk
command: >
qm set {{ vm_id }}
--serial0 socket
--boot order=scsi0
##################################################################
# 5. Optional Disk Resize
##################################################################
- name: Resize disk
command: qm resize {{ vm_id }} scsi0 {{ resize_size }}
when: resize_disk | default(false)
##################################################################
# 6. Optional GPU
##################################################################
- name: PCI GPU passthrough
command: qm set {{ vm_id }} --hostpci0 {{ gpu_device }}
when: gpu_passthrough | default(false)
- name: VirtIO GPU
command: qm set {{ vm_id }} --vga virtio
when: virtio_gpu | default(false)
##################################################################
# 7. Cloud-Init Snippets
##################################################################
- name: Create Cloud-Init vendor-data
template:
src: cloudinit_vendor.yaml.j2
dest: "/var/lib/vz/snippets/{{ vm_id }}-vendor.yaml"
- name: Create Cloud-Init user-data
template:
src: cloudinit_userdata.yaml.j2
dest: "/var/lib/vz/snippets/{{ vm_id }}-user.yaml"
- name: Write SSH key snippet
copy:
content: "{{ lookup('file', ssh_key_path) }}"
dest: "/var/lib/vz/snippets/{{ vm_id }}-sshkey.pub"
##################################################################
# 8. Apply Cloud-Init
##################################################################
- name: Apply Cloud-Init config
command: >
qm set {{ vm_id }}
--ciuser {{ ci_user }}
--sshkeys local:snippets/{{ vm_id }}-sshkey.pub
--hostname {{ hostname }}
--citype nocloud
--cicustom "user=local:snippets/{{ vm_id }}-user.yaml,vendor=local:snippets/{{ vm_id }}-vendor.yaml"
--ipconfig0 {{ ipconfig0 }}
##################################################################
# 9. Convert VM to Template
##################################################################
- name: Convert VM to template
command: qm template {{ vm_id }}
- name: "STAGE 5: Convert VM to template"
include_tasks: create-template.yml
tags: [template, create]
when: make_template | default(false)
args:
creates: "/etc/pve/qemu-server/{{ vm_id }}.conf.lock"
##################################################################
# 10. Create Clones (if enabled)
# 6. CREATE CLONES
##################################################################
- name: Create clones from template
- name: "STAGE 6: Create and configure clones"
include_tasks: create-clones.yml
tags: [clones, create]
when: create_clones | default(false)
loop: "{{ clones }}"
loop_control:
loop_var: clone
block:
- name: Check if clone exists
stat:
path: "/etc/pve/qemu-server/{{ clone.id }}.conf"
register: clone_conf
post_tasks:
- name: "Display completion summary"
debug:
msg: |
╔════════════════════════════════════════════════════════════╗
║ ✓ Playbook execution completed ║
║ ║
║ Template VM: {{ hostname }} (ID: {{ vm_id }}) ║
│ {% if make_template %}✓ Converted to template{% else %}✗ Template conversion disabled{% endif %}
│ {% if create_clones and clones %}✓ {{ clones | length }} clone(s) created{% else %}✗ Clone creation disabled{% endif %}
║ ║
║ Next steps: ║
║ - Verify VMs are running: qm list ║
║ - Connect to VM: ssh {{ ci_user }}@<vm-ip> ║
║ - Check Cloud-Init: cloud-init status ║
║ ║
╚════════════════════════════════════════════════════════════╝
- name: Clone VM from template
command: >
qm clone {{ vm_id }} {{ clone.id }} --name {{ clone.hostname }} --full {{ clone.full }}
when: not clone_conf.stat.exists
- name: Apply Cloud-Init settings for clone
command: >
qm set {{ clone.id }}
--hostname {{ clone.hostname }}
--ipconfig0 ip={{ clone.ip }},gw={{ clone.gateway }}
- name: Start clone VM
command: qm start {{ clone.id }}
rescue:
- name: "Handle playbook errors"
debug:
msg: |
✗ Playbook execution failed
Check the error messages above for details.
You may need to manually clean up partially created VMs.

167
tasks/main.yml.orig2 Normal file
View File

@@ -0,0 +1,167 @@
---
- name: "Create a Debian VM template and optionally deploy clones"
hosts: localhost
become: true
gather_facts: false
tasks:
##################################################################
# 1. Ensure Debian GenericCloud Image Exists
##################################################################
- name: Check for Debian image
stat:
path: "/var/lib/vz/template/qemu/debian-genericcloud-amd64.qcow2"
register: debian_img
- name: Download GenericCloud qcow2
get_url:
url: "https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-genericcloud-amd64.qcow2"
dest: "/var/lib/vz/template/qemu/debian-genericcloud-amd64.qcow2"
mode: "0644"
when: not debian_img.stat.exists
##################################################################
# 2. Create Base VM (if not exists)
##################################################################
- name: Check if VM exists
stat:
path: "/etc/pve/qemu-server/{{ vm_id }}.conf"
register: vm_conf
- name: Create VM
command: >
qm create {{ vm_id }}
--name {{ hostname }}
--memory {{ memory }}
--cores {{ cores }}
--cpu {{ cpu_type }}
--net0 virtio,bridge={{ bridge }},macaddr={{ mac_address }}
--agent 1
when: not vm_conf.stat.exists
##################################################################
# 3. Optional UEFI + Secure Boot + TPM
##################################################################
- name: Enable UEFI + TPM
command: >
qm set {{ vm_id }}
--bios ovmf
--efidisk0 {{ storage }}:0,pre-enrolled-keys=1
--tpmstate0 {{ storage }}:1,size=4M,version=v2.0
when: enable_tpm | default(false)
##################################################################
# 4. Disk Import & Attach
##################################################################
- name: Check if disk already exists
stat:
path: "/var/lib/vz/images/{{ vm_id }}/vm-{{ vm_id }}-disk-0.qcow2"
register: disk_exists
- name: Import qcow2 disk
command: >
qm importdisk {{ vm_id }}
/var/lib/vz/template/qemu/debian-genericcloud-amd64.qcow2
{{ storage }}
when: not disk_exists.stat.exists
- name: Attach imported disk
command: >
qm set {{ vm_id }}
--scsihw virtio-scsi-pci
--scsi0 {{ storage }}:vm-{{ vm_id }}-disk-0
when: not disk_exists.stat.exists
- name: Enable serial console + boot disk
command: >
qm set {{ vm_id }}
--serial0 socket
--boot order=scsi0
##################################################################
# 5. Optional Disk Resize
##################################################################
- name: Resize disk
command: qm resize {{ vm_id }} scsi0 {{ resize_size }}
when: resize_disk | default(false)
##################################################################
# 6. Optional GPU
##################################################################
- name: PCI GPU passthrough
command: qm set {{ vm_id }} --hostpci0 {{ gpu_device }}
when: gpu_passthrough | default(false)
- name: VirtIO GPU
command: qm set {{ vm_id }} --vga virtio
when: virtio_gpu | default(false)
##################################################################
# 7. Cloud-Init Snippets
##################################################################
- name: Create Cloud-Init vendor-data
template:
src: cloudinit_vendor.yaml.j2
dest: "/var/lib/vz/snippets/{{ vm_id }}-vendor.yaml"
- name: Create Cloud-Init user-data
template:
src: cloudinit_userdata.yaml.j2
dest: "/var/lib/vz/snippets/{{ vm_id }}-user.yaml"
- name: Write SSH key snippet
copy:
content: "{{ lookup('file', ssh_key_path) }}"
dest: "/var/lib/vz/snippets/{{ vm_id }}-sshkey.pub"
##################################################################
# 8. Apply Cloud-Init
##################################################################
- name: Apply Cloud-Init config
command: >
qm set {{ vm_id }}
--ciuser {{ ci_user }}
--sshkeys local:snippets/{{ vm_id }}-sshkey.pub
--hostname {{ hostname }}
--citype nocloud
--cicustom "user=local:snippets/{{ vm_id }}-user.yaml,vendor=local:snippets/{{ vm_id }}-vendor.yaml"
--ipconfig0 {{ ipconfig0 }}
##################################################################
# 9. Convert VM to Template
##################################################################
- name: Convert VM to template
command: qm template {{ vm_id }}
when: make_template | default(false)
args:
creates: "/etc/pve/qemu-server/{{ vm_id }}.conf.lock"
##################################################################
# 10. Create Clones (if enabled)
##################################################################
- name: Create clones from template
when: create_clones | default(false)
loop: "{{ clones }}"
loop_control:
loop_var: clone
block:
- name: Check if clone exists
stat:
path: "/etc/pve/qemu-server/{{ clone.id }}.conf"
register: clone_conf
- name: Clone VM from template
command: >
qm clone {{ vm_id }} {{ clone.id }} --name {{ clone.hostname }} --full {{ clone.full }}
when: not clone_conf.stat.exists
- name: Apply Cloud-Init settings for clone
command: >
qm set {{ clone.id }}
--hostname {{ clone.hostname }}
--ipconfig0 ip={{ clone.ip }},gw={{ clone.gateway }}
- name: Start clone VM
command: qm start {{ clone.id }}

117
tasks/preflight-checks.yml Normal file
View File

@@ -0,0 +1,117 @@
---
# preflight-checks.yml - Validate environment before running main tasks
- name: "[PREFLIGHT] Check if running on Proxmox host"
stat:
path: "/etc/pve/nodes"
register: pve_nodes
failed_when: not pve_nodes.stat.exists
changed_when: false
- name: "[PREFLIGHT] Verify qm command is available"
command: which qm
changed_when: false
failed_when: false
register: qm_check
- name: "[PREFLIGHT] Fail if qm not found"
fail:
msg: "qm command not found. This role requires Proxmox VE to be installed."
when: qm_check.rc != 0
- name: "[PREFLIGHT] Check if user can run qm commands"
command: qm version
changed_when: false
register: qm_version
- name: "[PREFLIGHT] Display Proxmox version"
debug:
msg: "Proxmox Version: {{ qm_version.stdout }}"
- name: "[PREFLIGHT] Verify storage pool exists"
command: "pvesm status {{ storage }}"
changed_when: false
failed_when: false
register: storage_check
- name: "[PREFLIGHT] Fail if storage not found"
fail:
msg: "Storage pool '{{ storage }}' not found. Available pools: run 'pvesm status'"
when: storage_check.rc != 0
- name: "[PREFLIGHT] Check SSH key file exists"
stat:
path: "{{ ssh_key_path | expanduser }}"
register: ssh_key_file
failed_when: not ssh_key_file.stat.exists
changed_when: false
- name: "[PREFLIGHT] Validate VM ID is unique"
command: "test ! -f /etc/pve/qemu-server/{{ vm_id }}.conf"
changed_when: false
failed_when: false
register: vm_id_check
- name: "[PREFLIGHT] Warn if VM ID already exists"
debug:
msg: "WARNING: VM ID {{ vm_id }} already exists. It will be skipped or updated."
when: vm_id_check.rc != 0
- name: "[PREFLIGHT] Validate clone IDs are unique"
command: "test ! -f /etc/pve/qemu-server/{{ item.id }}.conf"
changed_when: false
failed_when: false
loop: "{{ clones }}"
register: clone_id_checks
when: create_clones | default(false)
- name: "[PREFLIGHT] Warn if any clone IDs already exist"
debug:
msg: "WARNING: Clone ID {{ item.item.id }} already exists and will be skipped."
loop: "{{ clone_id_checks.results }}"
when: item.rc != 0 and create_clones | default(false)
- name: "[PREFLIGHT] Validate IP address format for clones"
assert:
that:
- "item.ip | ipaddr"
fail_msg: "Invalid IP address for clone {{ item.id }}: {{ item.ip }}"
loop: "{{ clones }}"
when: create_clones | default(false)
- name: "[PREFLIGHT] Validate static IP address format (if not DHCP)"
assert:
that:
- "ip_address | ipaddr"
fail_msg: "Invalid static IP address: {{ ip_address }}"
when: ip_mode == 'static'
- name: "[PREFLIGHT] Validate gateway IP address"
assert:
that:
- "gateway | ipaddr"
fail_msg: "Invalid gateway IP address: {{ gateway }}"
- name: "[PREFLIGHT] Validate DNS servers"
assert:
that:
- "item | ipaddr"
fail_msg: "Invalid DNS server IP: {{ item }}"
loop: "{{ dns }}"
when: dns is defined and dns | length > 0
- name: "[PREFLIGHT] Check snippets storage exists"
stat:
path: "/var/lib/vz/snippets"
register: snippets_dir
failed_when: not snippets_dir.stat.exists
changed_when: false
- name: "[PREFLIGHT] Summary - All checks passed"
debug:
msg: |
✓ Proxmox environment validated
✓ Storage pool '{{ storage }}' available
✓ SSH key found at {{ ssh_key_path }}
✓ VM ID {{ vm_id }} is available
✓ Ready to create VM: {{ hostname }}