Compare commits
24 Commits
cc72a0e412
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 5049990377 | |||
| d85c6afe5c | |||
| 38d30dca27 | |||
| 4792c24195 | |||
| 8082728c6e | |||
| a403f04500 | |||
| 701db06dc4 | |||
| 567b27264b | |||
| 9495475e58 | |||
| 1a0af3b6e6 | |||
| 8c031a915d | |||
| b8718a23a5 | |||
| 15325213ab | |||
| 6d7fc713a2 | |||
| 0c73433277 | |||
| 5762bd0a5e | |||
| 7ae3150ad0 | |||
| 900d376933 | |||
| 677fcf9916 | |||
| 7e3f26f38c | |||
| d91f90f06e | |||
| 326d4ac474 | |||
| bddcc72946 | |||
| a54e872993 |
@@ -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
94
tasks/clone_iteration.yml
Normal 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 %}
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -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) }}
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user