Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions scenarios/_reserved.json
Original file line number Diff line number Diff line change
Expand Up @@ -148,3 +148,7 @@
{"vm_id":9232,"vm_name":"template-vm-medium-02-8g-64g","spec":"2cpu/8gb/64gb","ip":"192.168.140.232","bridge":"vmbr140","scenario":"kunai_lab","role":"template"}
{"vm_id":1180,"vm_name":"admin-misp-standalone","ip":"192.168.142.180","role":"misp-lab","bridge":"vmbr142","scenario":"misp_lab"}
{"vm_id":9232,"vm_name":"template-vm-medium-02-8g-64g","spec":"2cpu/8gb/64gb","ip":"192.168.140.232","bridge":"vmbr140","scenario":"misp_lab","role":"template"}
{"bridge":"vmbr150","image":"debian_trixie","ip":"192.168.150.2","subnet":"lan1","vm_id":1002,"vm_name":"lan1-admin-rocketchat-00","scenario":"test_rocketchat"}
{"bridge":"vmbr151","image":"debian_trixie","ip":"192.168.151.2","subnet":"lan2","vm_id":3002,"vm_name":"lan2-debian-jump-00","scenario":"test_rocketchat"}
{"bridge":"vmbr140","image":"debian_trixie","ip":"192.168.140.3","spec":"1cpu/4gb/32gb","vm_id":9321,"vm_name":"template-vm-debian-trixie-small","scenario":"test_rocketchat","role":"template"}
{"bridge":"vmbr140","image":"debian_trixie","ip":"192.168.140.2","spec":"2cpu/8gb/64gb","vm_id":9331,"vm_name":"template-vm-debian-trixie-medium","scenario":"test_rocketchat","role":"template"}
8 changes: 8 additions & 0 deletions scenarios/test_rocketchat/01_init_proxmox/_main.reinstall.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/bin/bash
##
## re-run 01_init_proxmox (download images + create templates)
##

ansible-playbook -i "${RANGE42_ANSIBLE_ROLES__INVENTORY_DIR}/inventory_default.yml" \
-l "proxmox" \
"./_main.yml" --vault-password-file "${RANGE42_VAULT_PASSWORD_FILE:?RANGE42_VAULT_PASSWORD_FILE is not set — run: range42-context use <codename> <scenario>}"
2 changes: 2 additions & 0 deletions scenarios/test_rocketchat/01_init_proxmox/_main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- import_playbook: ./stage_00-download_cloudinit_files/_main.yml
- import_playbook: ./stage_01-create_templates/_main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### ####
# PROMOX INIT - download cloud-init base images (only the OS families used)
#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### ####

- import_playbook: ./cloudinit_debian_trixie.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### ####
#
# PROXMOX INIT - download cloud init image for debian_trixie
#
#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### ####

- hosts: proxmox
gather_facts: false
vars_files:
- "../../secrets/default_vault.yml"

tasks:
- name: PROMOX INIT - DOWNLOAD - CLOUD INIT images
include_role:
name: range42-ansible_roles-proxmox_controller

vars:
proxmox_vm_action: "storage_download_iso"
proxmox_storage: "local"
iso_file_content_type: "iso"
iso_url: "https://cloud.debian.org/images/cloud/trixie/latest/debian-13-genericcloud-amd64.raw"
iso_file_name: "debian-13-genericcloud-amd64.img"

# The download-url API is async (returns a UPID immediately). Poll on
# proxmox-cli until the file is fully on disk before stage_01 imports it.
- hosts: proxmox-cli
gather_facts: false
vars_files:
- "../../secrets/default_vault.yml"

tasks:
- name: PROMOX INIT - WAIT for debian-13-genericcloud-amd64.img to be fully downloaded
ansible.builtin.stat:
path: "/var/lib/vz/template/iso/debian-13-genericcloud-amd64.img"
register: _cloudinit_img
until: _cloudinit_img.stat.exists and _cloudinit_img.stat.size > 100000000
retries: 120 # 120 x 10 s = 20 min max
delay: 10
changed_when: false
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### ####
# PROMOX INIT - create the Proxmox template images (only the OS families used)
#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### ####

- import_playbook: ./templates/debian_trixie/_main_debian_trixie.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
##
## debian_trixie - apt proxy via cloud-init cicustom (optional)
##
## Attaches /var/lib/vz/snippets/range42-apt-proxy.yaml as cloud-init vendor-data
## to every template VM. All VMs cloned from these templates inherit the apt
## proxy automatically via cloud-init at first boot.
## Skipped (no-op) if apt_proxy_url is empty/unset. Idempotent.
##

- hosts: proxmox-cli
gather_facts: false
vars_files:
- "../../../../secrets/default_vault.yml"

vars:
bs2_template_vm_ids:
- 9321 # template-vm-debian-trixie-small
- 9331 # template-vm-debian-trixie-medium

tasks:
- name: APT-PROXY - SKIP (apt_proxy_url is empty)
ansible.builtin.debug:
msg: "apt_proxy_url is empty - cloud-init cicustom not applied to templates"
when: apt_proxy_url is not defined or (apt_proxy_url | length) == 0

- name: APT-PROXY - ATTACH cicustom vendor TO TEMPLATES
ansible.builtin.command:
cmd: qm set {{ item }} --cicustom "vendor=local:snippets/range42-apt-proxy.yaml"
loop: "{{ bs2_template_vm_ids }}"
register: qm_result
changed_when: qm_result.rc == 0
failed_when:
- qm_result.rc != 0
- "'does not exist' not in qm_result.stderr"
when: apt_proxy_url is defined and (apt_proxy_url | length) > 0

- name: APT-PROXY - DETACH cicustom vendor FROM TEMPLATES (apt_proxy_url unset)
ansible.builtin.command:
cmd: qm set {{ item }} --delete cicustom
loop: "{{ bs2_template_vm_ids }}"
register: qm_unset
changed_when: qm_unset.rc == 0
failed_when:
- qm_unset.rc != 0
- "'does not exist' not in qm_unset.stderr"
when: apt_proxy_url is not defined or (apt_proxy_url | length) == 0

- name: APT-PROXY - DISPLAY STATUS
ansible.builtin.debug:
msg: |
apt proxy: {{ apt_proxy_url if (apt_proxy_url is defined and (apt_proxy_url | length) > 0) else 'DISABLED' }}
cicustom : {{ 'attached to ' + (bs2_template_vm_ids | length | string) + ' templates' if (apt_proxy_url is defined and (apt_proxy_url | length) > 0) else 'detached from templates' }}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
##
## debian_trixie — create + configure all Proxmox template VMs for this image
##

- import_playbook: ./template-vm-debian-trixie-small.yml
- import_playbook: ./template-vm-debian-trixie-medium.yml

# apt proxy cicustom (no-op if apt_proxy_url empty)
- import_playbook: ./_apply_apt_proxy.yml

# pre-update: start each VM, run apt update + dist-upgrade via cloud-init poweroff,
# then convert to template.
- import_playbook: ./_update_templates.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
##
## debian_trixie - pre-update templates
##
## After all templates are created (but BEFORE convert to template), this playbook:
## 1. renders a temporary bootstrap snippet directly on the Proxmox host
## (apt update + apt dist-upgrade + cloud-init clean + auto-poweroff)
## 2. attaches it as cicustom vendor on each template VM
## 3. starts all templates
## 4. polls qm status until each VM is stopped (cloud-init auto-poweroff signals end)
## 5. detaches the bootstrap snippet, re-attaches the persistent apt-proxy snippet
## 6. converts each VM to a template (qm template <id>)
##
## Reads VM IDs from the scenario manifest (manifest/scenario_vms.json).
## Runs entirely on `proxmox-cli` (= the Proxmox host itself), no SSH involved.
##

- hosts: proxmox-cli
gather_facts: false
vars_files:
- "../../../../secrets/default_vault.yml"

vars:
manifest_path: "{{ playbook_dir }}/../../../../manifest/scenario_vms.json"
bootstrap_snippet_template: "{{ playbook_dir }}/range42-template-bootstrap.yaml.j2"
bootstrap_snippet_path: "/var/lib/vz/snippets/range42-template-bootstrap.yaml"

tasks:

#### #### #### debug apt_proxy_url visibility - fully safe vs undefined #### #### ####

- name: TEMPLATES UPDATE - DEBUG apt_proxy_url visibility
ansible.builtin.debug:
msg: |
apt_proxy_url defined : {{ apt_proxy_url is defined }}
apt_proxy_url value : {{ apt_proxy_url | default('(undefined)') }}
apt_proxy_url length : {{ (apt_proxy_url | default('')) | length }}
-> snippet WILL include apt section : {{ (apt_proxy_url | default('')) | length > 0 }}

#### #### #### load manifest #### #### ####

- name: TEMPLATES UPDATE - LOAD MANIFEST
ansible.builtin.set_fact:
templates: "{{ (lookup('file', manifest_path) | from_json).templates }}"

#### #### #### IDEMPOTENCE - probe each template's current state #### #### ####
# 3 possible states per VM ID :
# - "template" : already converted, skip update entirely
# - "vm" : exists as a regular VM, needs update + convert
# - "missing" : VM doesn't exist on Proxmox, skip (templates not created yet)
# Only "vm" state proceeds through the start/update/shutdown/convert pipeline.

- name: TEMPLATES UPDATE - PROBE state of each template VM
ansible.builtin.shell: |
if qm config {{ item.vm_id }} 2>/dev/null | grep -q '^template:'; then
echo "template"
elif qm config {{ item.vm_id }} >/dev/null 2>&1; then
echo "vm"
else
echo "missing"
fi
register: tpl_state
loop: "{{ templates }}"
loop_control:
label: "vm_id={{ item.vm_id }}"
changed_when: false

- name: TEMPLATES UPDATE - BUILD list of templates needing update (state == 'vm')
ansible.builtin.set_fact:
templates_to_update: >-
{{ templates | zip(tpl_state.results)
| selectattr('1.stdout', 'eq', 'vm')
| map(attribute='0') | list }}

- name: TEMPLATES UPDATE - DISPLAY PLAN
ansible.builtin.debug:
msg: |
total templates in manifest : {{ templates | length }}
already converted (skip) : {{ tpl_state.results | selectattr('stdout', 'eq', 'template') | list | length }}
missing (skip) : {{ tpl_state.results | selectattr('stdout', 'eq', 'missing') | list | length }}
to update + convert : {{ templates_to_update | length }}
{% if templates_to_update | length > 0 %}
targets :
{% for t in templates_to_update %} - {{ t.vm_id }} {{ t.vm_name }} ({{ t.ip }})
{% endfor %}
{% endif %}

- name: TEMPLATES UPDATE - SHORT-CIRCUIT if nothing to update
ansible.builtin.meta: end_play
when: templates_to_update | length == 0

#### #### #### render bootstrap snippet directly on the Proxmox host #### #### ####

- name: TEMPLATES UPDATE - RENDER BOOTSTRAP SNIPPET ON PROXMOX
ansible.builtin.template:
src: "{{ bootstrap_snippet_template }}"
dest: "{{ bootstrap_snippet_path }}"
mode: "0644"

#### #### #### attach bootstrap snippet on every template #### #### ####

- name: TEMPLATES UPDATE - ATTACH bootstrap cicustom
ansible.builtin.command:
cmd: qm set {{ item.vm_id }} --cicustom "vendor=local:snippets/range42-template-bootstrap.yaml"
loop: "{{ templates_to_update }}"
loop_control:
label: "vm_id={{ item.vm_id }}"

#### #### #### start all templates #### #### ####

- name: TEMPLATES UPDATE - START all templates
ansible.builtin.command:
cmd: qm start {{ item.vm_id }}
register: start_result
failed_when:
- start_result.rc != 0
- "'already running' not in start_result.stderr"
- "'MAX' not in start_result.stderr"
changed_when: start_result.rc == 0
loop: "{{ templates_to_update }}"
loop_control:
label: "vm_id={{ item.vm_id }}"

# VMs that failed with "MAX X vcpus" couldn't start - exclude them from
# the wait/convert pipeline but keep the play running for the others.
- name: TEMPLATES UPDATE - BUILD started list (exclude CPU-limited VMs)
ansible.builtin.set_fact:
templates_started: >-
{{ templates_to_update | zip(start_result.results)
| rejectattr('1.stderr', 'search', 'MAX')
| map(attribute='0') | list }}
templates_skipped_cpu: >-
{{ templates_to_update | zip(start_result.results)
| selectattr('1.stderr', 'search', 'MAX')
| map(attribute='0') | list }}

- name: TEMPLATES UPDATE - WARN skipped templates (host CPU limit)
ansible.builtin.debug:
msg: |
WARNING: {{ templates_skipped_cpu | length }} template(s) skipped - host CPU limit:
{% for t in templates_skipped_cpu %}
- {{ t.vm_id }} {{ t.vm_name }} (spec: {{ t.spec }})
{% endfor %}
These templates require more vCPUs than this host provides.
They will NOT be converted. Run on a host with enough physical
CPUs, or lower vm_cores in the template playbook if intentional.
when: templates_skipped_cpu | length > 0

#### #### #### wait until each VM auto-poweroffs (cloud-init done) #### #### ####
# the loop is sequential per-vm, but since all started in parallel above,
# total time is bounded by the slowest template (~3-15 min depending on cache)

- name: TEMPLATES UPDATE - WAIT for cloud-init to auto-poweroff
ansible.builtin.shell: |
qm status {{ item.vm_id }} | grep -q 'status: stopped'
register: stop_check
retries: 360 # safety cap : 360 x 5s = 30 min max per VM
delay: 5
until: stop_check.rc == 0
loop: "{{ templates_started }}"
loop_control:
label: "vm_id={{ item.vm_id }}"
changed_when: false

#### #### #### detach bootstrap, re-attach apt-proxy, convert to template #### #### ####

- name: TEMPLATES UPDATE - DETACH bootstrap cicustom
ansible.builtin.command:
cmd: qm set {{ item.vm_id }} --delete cicustom
loop: "{{ templates_started }}"
loop_control:
label: "vm_id={{ item.vm_id }}"

- name: TEMPLATES UPDATE - RE-ATTACH apt-proxy cicustom (persistent for clones)
ansible.builtin.command:
cmd: qm set {{ item.vm_id }} --cicustom "vendor=local:snippets/range42-apt-proxy.yaml"
loop: "{{ templates_started }}"
loop_control:
label: "vm_id={{ item.vm_id }}"
when: (apt_proxy_url | default('')) | length > 0

- name: TEMPLATES UPDATE - CONVERT VM to template
ansible.builtin.command:
cmd: qm template {{ item.vm_id }}
loop: "{{ templates_started }}"
loop_control:
label: "vm_id={{ item.vm_id }}"

#### #### #### cleanup #### #### ####

- name: TEMPLATES UPDATE - REMOVE BOOTSTRAP SNIPPET FROM PROXMOX
ansible.builtin.file:
path: "{{ bootstrap_snippet_path }}"
state: absent

- name: TEMPLATES UPDATE - DISPLAY DONE
ansible.builtin.debug:
msg: |
{{ templates_started | length }} templates updated and converted
apt-proxy cicustom re-attached (if apt_proxy_url set)
{% if templates_skipped_cpu | length > 0 %}
{{ templates_skipped_cpu | length }} skipped (host CPU limit) - see warning above
{% endif %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#cloud-config
# range42 - TEMPORARY bootstrap snippet for templates
# Triggers apt update + dist-upgrade then poweroff.
# Auto-generated by _update_templates.yml. Detached after the template is
# converted, replaced by range42-apt-proxy.yaml (persistent).
#
# NOTE: cloud-init clean was previously here in `runcmd` - REMOVED because
# it ran in cloud_config stage, before cloud_final, and broke
# package-update-upgrade-install. Clones get a new vmid -> new instance-id,
# so cloud-init re-processes naturally on each clone. No clean needed.
#
{% if (apt_proxy_url | default('')) | length > 0 %}
apt:
http_proxy: "{{ apt_proxy_url }}"
{% endif %}
package_update: true
package_upgrade: true
package_reboot_if_required: false
power_state:
mode: poweroff
timeout: 60
condition: True
Loading