Compare commits

...

24 Commits

Author SHA1 Message Date
5049990377 fix: refactor clone creation logic into separate iteration file 2025-12-06 11:53:45 +01:00
d85c6afe5c fix: update Cloud-Init snippet SSH key path for Proxmox configuration 2025-12-06 09:45:45 +01:00
38d30dca27 fix: update snippet enabling logic for Proxmox storage configuration 2025-12-06 09:34:58 +01:00
4792c24195 fix: refine regex to limit snippet insertion to specific Proxmox storage 2025-12-06 09:28:23 +01:00
8082728c6e fix: enhance regex for enabling 'snippets' in Proxmox storage configuration 2025-12-06 09:25:17 +01:00
a403f04500 fix: update regex for Proxmox storage configuration to ensure 'snippets' is included 2025-12-06 09:17:45 +01:00
701db06dc4 fix: ensure 'snippets' is enabled for Proxmox storage configuration 2025-12-06 09:12:38 +01:00
567b27264b fix: ensure Proxmox storage supports snippets and verify directory existence 2025-12-06 09:08:18 +01:00
9495475e58 fix: streamline validation for Proxmox storage snippets support 2025-12-06 08:55:51 +01:00
1a0af3b6e6 fix: refine content extraction for Proxmox snippets storage configuration 2025-12-06 08:44:42 +01:00
8c031a915d fix: update preflight checks to ensure 'snippets' is included in Proxmox storage content list 2025-12-06 08:40:48 +01:00
b8718a23a5 fix: improve condition checks for Proxmox snippets storage configuration 2025-12-06 08:14:03 +01:00
15325213ab fix: add support for Proxmox snippets storage configuration and update related paths 2025-12-06 08:08:26 +01:00
6d7fc713a2 fix: add optional FQDN configuration to VM template and Cloud-Init user-data 2025-12-06 07:06:38 +01:00
0c73433277 fix: update SSH public key copy task to use remote source 2025-12-05 18:49:42 +01:00
5762bd0a5e fix: update SSH key handling in Cloud-Init user-data template 2025-12-05 18:33:23 +01:00
7ae3150ad0 fix: verify SSH key readability before creating Cloud-Init user-data snippet 2025-12-03 21:14:28 +01:00
900d376933 fix: update SSH key path variable to use ssh_keys_file in configuration tasks 2025-12-03 20:55:47 +01:00
677fcf9916 fix: change become to false for pip installation task in preflight checks 2025-12-03 20:48:54 +01:00
7e3f26f38c feat: add task to ensure passlib is installed on the ansible controller 2025-12-03 20:47:00 +01:00
d91f90f06e fix: update last modified timestamp format in download-image.yml 2025-12-03 18:20:17 +01:00
326d4ac474 refactor: remove debug output for mtime and format last modified timestamp 2025-12-03 18:17:54 +01:00
bddcc72946 modified: tasks/download-image.yml 2025-12-03 18:14:19 +01:00
a54e872993 modified: tasks/download-image.yml 2025-12-03 18:10:53 +01:00
8 changed files with 164 additions and 135 deletions

View File

@@ -13,6 +13,10 @@ vm_id: 150
# Hostname for the base VM (template)
hostname: debian-template-base
# optional fqdn
# fqdn: myvm.example.com
# Memory in MB
memory: 2048
@@ -28,6 +32,10 @@ bridge: vmbr0
# Proxmox storage pool for VM disks
storage: local-lvm
# Proxmox storage pool for snippets
proxmox_snippets_storage: local
proxmox_snippets_storage_path: /var/lib/vz
###############################################################################
# MAC ADDRESS GENERATION (avoids collisions)
###############################################################################
@@ -48,8 +56,8 @@ mac_address: "{{ mac_base }}:{{ mac_suffix }}"
debian_image_url: "https://cloud.debian.org/images/cloud/trixie/latest/debian-13-genericcloud-amd64.qcow2"
# Local path where image is cached
# debian_image_path: "/var/lib/vz/template/qemu/debian-genericcloud-amd64.qcow2"
debian_image_path: "/var/lib/vz/template/qemu/debian-13-genericcloud-amd64.qcow2"
# debian_image_path: "{{ proxmox_snippets_storage_path }}/template/qemu/debian-genericcloud-amd64.qcow2"
debian_image_path: "{{ proxmox_snippets_storage_path }}/template/qemu/debian-13-genericcloud-amd64.qcow2"
###############################################################################
# NETWORKING CONFIGURATION

94
tasks/clone_iteration.yml Normal file
View File

@@ -0,0 +1,94 @@
---
# clone_iteration.yml - Single clone iteration (called from create-clones.yml loop)
- name: "[CLONES] Check if clone already exists"
ansible.builtin.include_tasks: helpers.yml
vars:
helper_task: check_vm_exists
target_vm_id: "{{ clone.id }}"
- name: "[CLONES] Display clone status"
ansible.builtin.debug:
msg: "Clone {{ clone.id }} ({{ clone.hostname }}) - Status: {{ 'EXISTS' if vm_exists else 'WILL BE CREATED' }}"
- name: "[CLONES] Clone VM from template"
block:
- name: "[CLONES] Execute clone command"
ansible.builtin.command: >
qm clone {{ vm_id }} {{ clone.id }}
--name {{ clone.hostname }}
--full {{ clone.full | default(0) }}
register: clone_cmd
when: not vm_exists
- name: "[CLONES] Verify clone was created"
ansible.builtin.include_tasks: helpers.yml
vars:
helper_task: check_vm_exists
target_vm_id: "{{ clone.id }}"
when: not vm_exists
- name: "[CLONES] Ensure clone creation succeeded"
ansible.builtin.assert:
that:
- vm_exists | bool
fail_msg: "Failed to create clone {{ clone.id }}"
when: not vm_exists
- name: "[CLONES] Wait for clone to be ready"
ansible.builtin.pause:
seconds: 2
when: not vm_exists
rescue:
- name: "[CLONES] Handle clone creation error"
ansible.builtin.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"
ansible.builtin.command: >
qm set {{ clone.id }}
--hostname {{ clone.hostname }}
--ipconfig0 "ip={{ clone.ip }},gw={{ clone.gateway }}"
register: clone_config
when: not vm_exists
- name: "[CLONES] Apply SSH keys to clone"
ansible.builtin.command: >
qm set {{ clone.id }}
--sshkeys local:snippets/{{ vm_id }}-sshkey.pub
when: not vm_exists
rescue:
- name: "[CLONES] Handle clone configuration error"
ansible.builtin.debug:
msg: "WARNING: Could not fully configure clone {{ clone.id }}. You may need to configure manually."
- name: "[CLONES] Start clone VM"
ansible.builtin.command: "qm start {{ clone.id }}"
register: clone_start
retries: "{{ max_retries }}"
delay: "{{ retry_delay }}"
until: clone_start is succeeded
when: not vm_exists
- name: "[CLONES] Wait for clone to boot"
ansible.builtin.pause:
seconds: 3
- name: "[CLONES] Display clone creation result"
ansible.builtin.debug:
msg: |
{% if vm_exists %}
Clone {{ clone.id }} ({{ clone.hostname }}) already exists - skipped
{% else %}
✓ Clone created and started
- ID: {{ clone.id }}
- Hostname: {{ clone.hostname }}
- IP: {{ clone.ip }}
- Full clone: {{ clone.full | default(0) }}
{% endif %}

View File

@@ -126,27 +126,28 @@
- name: "[CONFIG] Create Cloud-Init vendor-data snippet"
ansible.builtin.template:
src: cloudinit_vendor.yaml.j2
dest: "/var/lib/vz/snippets/{{ vm_id }}-vendor.yaml"
dest: "{{ proxmox_snippets_storage_path }}/snippets/{{ vm_id }}-vendor.yaml"
mode: "0644"
register: vendor_snippet
- name: "[CONFIG] Verify SSH key is readable"
ansible.builtin.stat:
path: "{{ ssh_keys_file | expanduser }}"
register: ssh_key_stat
failed_when: not ssh_key_stat.stat.readable
- name: "[CONFIG] Create Cloud-Init user-data snippet"
ansible.builtin.template:
src: cloudinit_userdata.yaml.j2
dest: "/var/lib/vz/snippets/{{ vm_id }}-user.yaml"
dest: "{{ proxmox_snippets_storage_path }}/snippets/{{ vm_id }}-user.yaml"
mode: "0644"
register: user_snippet
- name: "[CONFIG] Verify SSH key is readable"
ansible.builtin.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"
ansible.builtin.copy:
src: "{{ ssh_key_path | expanduser }}"
dest: "/var/lib/vz/snippets/{{ vm_id }}-sshkey.pub"
src: "{{ ssh_keys_file }}"
dest: "{{ proxmox_snippets_storage_path }}/snippets/{{ vm_id }}-sshkey.pub"
remote_src: true
mode: "0644"
register: ssh_snippet
@@ -154,8 +155,7 @@
ansible.builtin.command: >
qm set {{ vm_id }}
--ciuser {{ ci_user }}
--sshkeys local:snippets/{{ vm_id }}-sshkey.pub
--hostname {{ hostname }}
--sshkeys "{{ proxmox_snippets_storage_path }}/snippets/{{ vm_id }}-sshkey.pub"
--citype nocloud
--cicustom "user=local:snippets/{{ vm_id }}-user.yaml,vendor=local:snippets/{{ vm_id }}-vendor.yaml"
--ipconfig0 {{ ipconfig0 }}

View File

@@ -9,102 +9,10 @@
- clones is not defined or clones | length == 0
- name: "[CLONES] Process each clone"
block:
- name: "[CLONES] Check if clone already exists"
ansible.builtin.include_tasks: helpers.yml
include_tasks: clone_iteration.yml
vars:
helper_task: check_vm_exists
target_vm_id: "{{ clone.id }}"
- name: "[CLONES] Display clone status"
ansible.builtin.debug:
msg: "Clone {{ clone.id }} ({{ clone.hostname }}) - Status: {{ 'EXISTS' if vm_exists else 'WILL BE CREATED' }}"
- name: "[CLONES] Clone VM from template"
block:
- name: "[CLONES] Execute clone command"
ansible.builtin.command: >
qm clone {{ vm_id }} {{ clone.id }}
--name {{ clone.hostname }}
--full {{ clone.full | default(0) }}
register: clone_cmd
when: not vm_exists
- name: "[CLONES] Verify clone was created"
ansible.builtin.include_tasks: helpers.yml
vars:
helper_task: check_vm_exists
target_vm_id: "{{ clone.id }}"
when: not vm_exists
- name: "[CLONES] Ensure clone creation succeeded"
ansible.builtin.assert:
that:
- vm_exists | bool
fail_msg: "Failed to create clone {{ clone.id }}"
when: not vm_exists
- name: "[CLONES] Wait for clone to be ready"
ansible.builtin.pause:
seconds: 2
when: not vm_exists
rescue:
- name: "[CLONES] Handle clone creation error"
ansible.builtin.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"
ansible.builtin.command: >
qm set {{ clone.id }}
--hostname {{ clone.hostname }}
--ipconfig0 "ip={{ clone.ip }},gw={{ clone.gateway }}"
register: clone_config
when: not vm_exists
- name: "[CLONES] Apply SSH keys to clone"
ansible.builtin.command: >
qm set {{ clone.id }}
--sshkeys local:snippets/{{ vm_id }}-sshkey.pub
when: not vm_exists
rescue:
- name: "[CLONES] Handle clone configuration error"
ansible.builtin.debug:
msg: "WARNING: Could not fully configure clone {{ clone.id }}. You may need to configure manually."
- name: "[CLONES] Start clone VM"
ansible.builtin.command: "qm start {{ clone.id }}"
register: clone_start
retries: "{{ max_retries }}"
delay: "{{ retry_delay }}"
until: clone_start is succeeded
when: not vm_exists
- name: "[CLONES] Wait for clone to boot"
ansible.builtin.pause:
seconds: 3
- name: "[CLONES] Display clone creation result"
ansible.builtin.debug:
msg: |
{% if vm_exists %}
Clone {{ clone.id }} ({{ clone.hostname }}) already exists - skipped
{% else %}
✓ Clone created and started
- ID: {{ clone.id }}
- Hostname: {{ clone.hostname }}
- IP: {{ clone.ip }}
- Full clone: {{ clone.full | default(0) }}
{% endif %}
clone: "{{ item }}"
loop: "{{ clones }}"
loop_control:
loop_var: clone
when: create_clones | default(false)
- name: "[CLONES] Skip clone creation (disabled)"

View File

@@ -9,7 +9,7 @@
- name: "[IMAGE] Create template directory if missing"
ansible.builtin.file:
path: "/var/lib/vz/template/qemu"
path: "{{ proxmox_snippets_storage_path }}/template/qemu"
state: directory
mode: "0755"
when: not debian_img.stat.exists
@@ -33,15 +33,9 @@
changed_when: false
failed_when: not debian_img_final.stat.exists or debian_img_final.stat.size == 0
- name: Debug mtime type
debug:
msg: |
mtime={{ debian_img_final.stat.mtime }} ({{ debian_img_final.stat.mtime | type_debug }})
{{ (debian_img_final.stat.mtime // 1000)|timestamp_to_time|datetimeformat('%Y-%m-%d %H:%M:%S') }}
- name: "[IMAGE] Display image info"
ansible.builtin.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 | int | strftime('%Y-%m-%d %H:%M:%S') }}
Last modified: {{ '%d/%m/%Y -%H:%M:%S' | strftime(debian_img_final.stat.mtime) }}

View File

@@ -142,9 +142,9 @@
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"
- "{{ proxmox_snippets_storage_path }}/snippets/{{ target_vm_id }}-user.yaml"
- "{{ proxmox_snippets_storage_path }}/snippets/{{ target_vm_id }}-vendor.yaml"
- "{{ proxmox_snippets_storage_path }}/snippets/{{ target_vm_id }}-sshkey.pub"
when: helper_task == "cleanup_snippets"

View File

@@ -15,6 +15,15 @@
run_once: true
become: false
- name: "[PREFLIGHT] Ensure passlib is installed"
ansible.builtin.pip:
name: passlib
state: present
executable: "{{ controller_python | dirname }}/pip"
delegate_to: localhost
run_once: true
become: false
- name: "[PREFLIGHT] Check if running on Proxmox host"
ansible.builtin.stat:
path: "/etc/pve/nodes"
@@ -70,7 +79,6 @@
failed_when: not ssh_key_file.stat.exists
changed_when: false
- name: "[PREFLIGHT] Validate VM ID is unique"
ansible.builtin.command: "test ! -f /etc/pve/qemu-server/{{ vm_id }}.conf"
changed_when: false
@@ -149,24 +157,34 @@
loop: "{{ dns }}"
when: dns is defined and dns | length > 0
- name: "[PREFLIGHT] Ensure snippets storage exists"
- name: "[PREFLIGHT] Ensure Proxmox storage supports snippets"
block:
- name: "[PREFLIGHT] Ensure 'snippets' is enabled for {{ proxmox_snippets_storage }}"
ansible.builtin.replace:
path: /etc/pve/storage.cfg
regexp: '(dir:\s*{{ proxmox_snippets_storage }}[\s\S]*?content\s+)(.*)(?<!snippets)'
replace: '\1\2,snippets'
- name: "[PREFLIGHT] Ensure snippets storage directory exists"
ansible.builtin.file:
path: "/var/lib/vz/snippets"
path: "{{ proxmox_snippets_storage_path }}/snippets"
state: directory
mode: "0755"
- name: "[PREFLIGHT] Check snippets storage exists"
- name: "[PREFLIGHT] Verify snippets storage directory exists"
ansible.builtin.stat:
path: "/var/lib/vz/snippets"
path: "{{ proxmox_snippets_storage_path }}/snippets"
register: snippets_dir
failed_when: not snippets_dir.stat.exists
changed_when: false
become: true
- name: "[PREFLIGHT] Summary - All checks passed"
ansible.builtin.debug:
msg: |
✓ Proxmox environment validated
✓ Storage pool '{{ storage }}' available
✓ Storage pool '{{ storage }}' available for VM disks
✓ Storage pool '{{ proxmox_snippets_storage }}' available for snippets
✓ SSH key found at {{ ssh_key_path }}
✓ VM ID {{ vm_id }} is available
✓ Ready to create VM: {{ hostname }}

View File

@@ -1,5 +1,10 @@
#cloud-config
hostname: {{ hostname }}
{% if domain is defined and domain %}
fqdn: {{ hostname }}.{{ domain }}
{% endif %}
users:
- name: {{ ci_user }}
sudo: ALL=(ALL) NOPASSWD:ALL
@@ -8,7 +13,9 @@ users:
lock_passwd: false
passwd: {{ ci_password | password_hash('sha512') }}
ssh_authorized_keys:
- {{ lookup('file', ssh_key_path) }}
{% for key in ssh_public_keys %}
- {{ key }}
{% endfor %}
chpasswd:
expire: false