big fat commit with everything working !

This commit is contained in:
Ludovic Cartier
2026-05-29 18:30:16 +02:00
parent e3b317fa56
commit 3276afa34c
10 changed files with 674 additions and 0 deletions
+82
View File
@@ -0,0 +1,82 @@
---
- name: deploy-vm | check mandatory variables
ansible.builtin.assert:
that:
- proxmox_host is defined
- proxmox_host | length > 0
- proxmox_node is defined
- proxmox_node | length > 0
- proxmox_storage is defined
- proxmox_storage | length > 0
- proxmox_api_user is defined
- proxmox_api_user | length > 0
- proxmox_api_password is defined
- proxmox_api_password | length > 0
- template_name is defined
- template_name | length > 0
- vm_name is defined
- vm_name | length > 0
- vm_ip is defined
- vm_ip | length > 0
- vm_ip is match('^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/[0-9]+$')
- vm_disk_size is defined
- vm_disk_size | length >= 0
fail_msg: >-
Variables obligatoires manquantes ou invalides.
vm_name (chaîne non vide) et vm_ip (format CIDR, ex: 192.168.1.100/24) sont requis.
success_msg: "Pré-vérifications OK — VM: {{ vm_name }} | IP: {{ vm_ip }}"
- name: deploy-vm | assert vm_disk_size format is valid
ansible.builtin.assert:
that: vm_disk_size is match('^\+?[0-9]+[KMGT]$')
fail_msg: >-
vm_disk_size='{{ vm_disk_size }}' est invalide.
Format attendu : nombre suivi d'une unité (ex: "50G", "500M", "+10G").
Sans unité, Proxmox interprète la valeur en kB et refusera de "réduire" le disque.
when: vm_disk_size | length > 0
- name: deploy-vm | assert extra disks format
ansible.builtin.assert:
that:
- _disk.disk is defined
- _disk.disk | length > 0
- _disk.size is defined
- _disk.size is match('^[0-9]+[KMGT]$')
fail_msg: >-
Disque supplémentaire invalide : {{ _disk }}.
'disk' (ex: scsi1) et 'size' (ex: 100G) sont obligatoires.
La taille doit être absolue (pas de préfixe +).
loop: "{{ vm_extra_disks }}"
loop_control:
loop_var: _disk
label: "{{ _disk.disk | default('?') }}"
when: vm_extra_disks | length > 0
# Vérifie l'existence de la VM cible dans le cluster (résultat réutilisé dans clone.yml)
- name: deploy-vm | check if VM '{{ vm_name }}' already exists
community.general.proxmox_vm_info:
api_host: "{{ proxmox_host }}"
api_user: "{{ proxmox_api_user }}"
api_password: "{{ proxmox_api_password }}"
name: "{{ vm_name }}"
type: qemu
register: existing_vm_info
- name: deploy-vm | fail — VM exists, vm_force_update required to reconfigure
ansible.builtin.fail:
msg: |
La VM '{{ vm_name }}' existe déjà.
VMID : {{ existing_vm_info.proxmox_vms[0].vmid }}
Nœud : {{ existing_vm_info.proxmox_vms[0].node }}
Statut : {{ existing_vm_info.proxmox_vms[0].status }}
Modifications demandées :
Cœurs : {{ existing_vm_info.proxmox_vms[0].maxcpu | default('?') }} → {{ vm_cores }}
RAM : {{ (existing_vm_info.proxmox_vms[0].maxmem | default(0) | int // 1048576) }} Mo → {{ vm_memory }} Mo
IP : {{ vm_ip }}
Pour appliquer ces modifications, relancez avec : -e vm_force_update=true
when:
- existing_vm_info.proxmox_vms | length > 0
- not (vm_force_update | default(false) | bool)
+48
View File
@@ -0,0 +1,48 @@
---
# Localise le template dans le cluster (sans contrainte de nœud → recherche globale)
- name: deploy-vm | find template '{{ template_name }}' in cluster
community.general.proxmox_vm_info:
api_host: "{{ proxmox_host }}"
api_user: "{{ proxmox_api_user }}"
api_password: "{{ proxmox_api_password }}"
name: "{{ template_name }}"
type: qemu
register: template_info
- name: deploy-vm | assert template '{{ template_name }}' exists
ansible.builtin.assert:
that: template_info.proxmox_vms | length > 0
fail_msg: "Template '{{ template_name }}' introuvable dans le cluster Proxmox."
# node = nœud du template (requis par l'API Proxmox pour localiser la source du clone)
- name: deploy-vm | clone template '{{ template_name }}' → '{{ vm_name }}'
community.general.proxmox_kvm:
api_host: "{{ proxmox_host }}"
api_user: "{{ proxmox_api_user }}"
api_password: "{{ proxmox_api_password }}"
node: "{{ template_info.proxmox_vms[0].node }}"
clone: "{{ template_name }}"
newid: "{{ vm_id | default(omit, true) }}"
name: "{{ vm_name }}"
full: "{{ vm_full_clone }}"
storage: "{{ proxmox_storage }}"
timeout: "{{ vm_wait_timeout }}"
state: present
register: clone_result
when: existing_vm_info.proxmox_vms | length == 0
# resolved_node = nœud réel de la VM (existante ou fraîchement clonée)
- name: deploy-vm | resolve VM ID and node
ansible.builtin.set_fact:
resolved_vm_id: >-
{{
existing_vm_info.proxmox_vms[0].vmid
if existing_vm_info.proxmox_vms | length > 0
else clone_result.vmid
}}
resolved_node: >-
{{
existing_vm_info.proxmox_vms[0].node
if existing_vm_info.proxmox_vms | length > 0
else template_info.proxmox_vms[0].node
}}
+88
View File
@@ -0,0 +1,88 @@
---
# Applique la configuration hardware + cloud-init sur la VM clonée
- name: deploy-vm | configure resources and cloud-init for '{{ vm_name }}'
community.general.proxmox_kvm:
api_host: "{{ proxmox_host }}"
api_user: "{{ proxmox_api_user }}"
api_password: "{{ proxmox_api_password }}"
node: "{{ resolved_node }}"
vmid: "{{ resolved_vm_id }}"
name: "{{ vm_name }}"
cores: "{{ vm_cores }}"
sockets: "{{ vm_sockets }}"
memory: "{{ vm_memory }}"
onboot: "{{ vm_start_on_boot }}"
ipconfig:
ipconfig0: "ip={{ vm_ip }},gw={{ vm_gateway }}"
nameservers: "{{ vm_dns }}"
ciuser: "{{ vm_ciuser | default(omit, true) }}"
cipassword: "{{ vm_cipassword | default(omit, true) }}"
sshkeys: "{{ vm_sshkeys | default(omit, true) }}"
state: present
update: true
- name: deploy-vm | resize disk '{{ vm_disk_name }}' → {{ vm_disk_size }}
community.general.proxmox_disk:
api_host: "{{ proxmox_host }}"
api_user: "{{ proxmox_api_user }}"
api_password: "{{ proxmox_api_password }}"
vmid: "{{ resolved_vm_id }}"
disk: "{{ vm_disk_name }}"
size: "{{ vm_disk_size }}"
state: resized
when: vm_disk_size | length > 0
# Création des disques supplémentaires via l'API Proxmox directement (uri).
# Raison : proxmox_kvm update:true n'envoie pas les paramètres disque à l'API
# ("no options specified"), et proxmox_disk state:present envoie 'storage:20G'
# qui est invalide pour Ceph/RBD.
# L'appel PUT /config avec 'storage:0,size=XG' est compatible avec tous les backends.
# Idempotence : on lit la config courante au préalable et on saute les slots déjà occupés.
- name: deploy-vm | get Proxmox API ticket for extra disk creation
ansible.builtin.uri:
url: "https://{{ proxmox_host }}:8006/api2/json/access/ticket"
method: POST
body_format: form-urlencoded
body:
username: "{{ proxmox_api_user }}"
password: "{{ proxmox_api_password }}"
validate_certs: "{{ proxmox_validate_certs }}"
register: _proxmox_disk_auth
no_log: true
when: vm_extra_disks | length > 0
- name: deploy-vm | read current VM config (idempotence check for extra disks)
ansible.builtin.uri:
url: "https://{{ proxmox_host }}:8006/api2/json/nodes/{{ resolved_node }}/qemu/{{ resolved_vm_id }}/config"
method: GET
headers:
Cookie: "PVEAuthCookie={{ _proxmox_disk_auth.json.data.ticket }}"
validate_certs: "{{ proxmox_validate_certs }}"
register: _vm_current_config
when: vm_extra_disks | length > 0
- name: "deploy-vm | add extra disk '{{ _disk.disk }}' ({{ _disk.size }})"
vars:
_opts: "{{ _disk.storage | default(proxmox_storage) }}:0,size={{ _disk.size\
}}{{ ',ssd=1' if _disk.ssd | default(false) | bool else ''\
}}{{ ',iothread=1' if _disk.iothread | default(false) | bool else ''\
}}{{ ',backup=0' if not (_disk.backup | default(true) | bool) else ''\
}}{{ ',cache=' + _disk.cache if _disk.cache | default('') | length > 0 else '' }}"
ansible.builtin.uri:
url: "https://{{ proxmox_host }}:8006/api2/json/nodes/{{ resolved_node }}/qemu/{{ resolved_vm_id }}/config"
method: PUT
headers:
Cookie: "PVEAuthCookie={{ _proxmox_disk_auth.json.data.ticket }}"
CSRFPreventionToken: "{{ _proxmox_disk_auth.json.data.CSRFPreventionToken }}"
body_format: form-urlencoded
body: "{{ {_disk.disk: _opts} }}"
validate_certs: "{{ proxmox_validate_certs }}"
status_code: [200]
loop: "{{ vm_extra_disks }}"
loop_control:
loop_var: _disk
label: "{{ _disk.disk }} ({{ _disk.size }})"
when:
- vm_extra_disks | length > 0
- _disk.disk not in (_vm_current_config.json.data | default({}))
+11
View File
@@ -0,0 +1,11 @@
---
# Pipeline de déploiement d'une seule VM.
# Appelé par main.yml, soit directement (mode single-VM), soit en boucle (mode multi-VMs).
# Les variables vm_* sont soit déclarées dans le playbook appelant,
# soit injectées par le loop de main.yml via include_tasks vars:.
# include_tasks (et non import_tasks) est requis pour hériter des vars du parent.
- ansible.builtin.include_tasks: asserts.yml
- ansible.builtin.include_tasks: clone.yml
- ansible.builtin.include_tasks: migrate.yml
- ansible.builtin.include_tasks: configure.yml
- ansible.builtin.include_tasks: start.yml
+55
View File
@@ -0,0 +1,55 @@
---
# Capture les valeurs globales avant la boucle pour éviter la récursion infinie.
# Sans ça, "vm_cores: {{ item.cores | default(vm_cores) }}" → Ansible évalue vm_cores
# qui pointe vers lui-même (le task var en cours de définition) → boucle infinie.
- name: deploy-vm | snapshot global defaults before VM loop
ansible.builtin.set_fact:
_g_proxmox_node: "{{ proxmox_node }}"
_g_template_name: "{{ template_name }}"
_g_vm_cores: "{{ vm_cores }}"
_g_vm_sockets: "{{ vm_sockets }}"
_g_vm_memory: "{{ vm_memory }}"
_g_vm_gateway: "{{ vm_gateway }}"
_g_vm_dns: "{{ vm_dns }}"
_g_vm_disk_name: "{{ vm_disk_name }}"
_g_vm_disk_size: "{{ vm_disk_size }}"
_g_vm_extra_disks: "{{ vm_extra_disks }}"
_g_vm_ciuser: "{{ vm_ciuser }}"
_g_vm_cipassword: "{{ vm_cipassword }}"
_g_vm_sshkeys: "{{ vm_sshkeys }}"
_g_vm_full_clone: "{{ vm_full_clone }}"
_g_vm_start_on_boot: "{{ vm_start_on_boot }}"
_g_vm_wait_timeout: "{{ vm_wait_timeout }}"
_g_vm_force_update: "{{ vm_force_update }}"
_g_vm_migrate_with_local_disks: "{{ vm_migrate_with_local_disks }}"
# Chaque entrée de 'vms' peut surcharger n'importe quel champ.
# Champs obligatoires : name, ip.
# Tous les autres héritent du snapshot global ci-dessus.
- name: "deploy-vm | deploy '{{ item.name }}'"
ansible.builtin.include_tasks: deploy_one.yml
vars:
vm_name: "{{ item.name }}"
vm_id: "{{ item.id | default(None) }}"
vm_cores: "{{ item.cores | default(_g_vm_cores) }}"
vm_sockets: "{{ item.sockets | default(_g_vm_sockets) }}"
vm_memory: "{{ item.memory | default(_g_vm_memory) }}"
vm_ip: "{{ item.ip }}"
vm_gateway: "{{ item.gateway | default(_g_vm_gateway) }}"
vm_dns: "{{ item.dns | default(_g_vm_dns) }}"
vm_disk_name: "{{ item.disk_name | default(_g_vm_disk_name) }}"
vm_disk_size: "{{ item.disk_size | default(_g_vm_disk_size) }}"
vm_extra_disks: "{{ item.extra_disks | default(_g_vm_extra_disks) }}"
vm_ciuser: "{{ item.ciuser | default(_g_vm_ciuser) }}"
vm_cipassword: "{{ item.cipassword | default(_g_vm_cipassword) }}"
vm_sshkeys: "{{ item.sshkeys | default(_g_vm_sshkeys) }}"
vm_full_clone: "{{ item.full_clone | default(_g_vm_full_clone) }}"
vm_start_on_boot: "{{ item.start_on_boot | default(_g_vm_start_on_boot) }}"
vm_wait_timeout: "{{ item.wait_timeout | default(_g_vm_wait_timeout) }}"
vm_force_update: "{{ item.force_update | default(_g_vm_force_update) }}"
vm_migrate_with_local_disks: "{{ item.migrate_with_local_disks | default(_g_vm_migrate_with_local_disks) }}"
proxmox_node: "{{ item.proxmox_node | default(_g_proxmox_node) }}"
template_name: "{{ item.template_name | default(_g_template_name) }}"
loop: "{{ vms }}"
loop_control:
label: "{{ item.name }}"
+65
View File
@@ -0,0 +1,65 @@
---
# Migration optionnelle vers proxmox_node si différent du nœud du template.
# Déclenchée uniquement si proxmox_node est défini et != resolved_node (nœud du clone).
# La VM doit être arrêtée — ce qui est le cas entre le clone et le démarrage.
- name: deploy-vm | migrate VM to target node
when:
- proxmox_node | default('') | length > 0
- proxmox_node != resolved_node
block:
# Proxmox impose un ticket de session pour les appels POST directs (CSRF)
- name: deploy-vm | authenticate to Proxmox API (migration)
ansible.builtin.uri:
url: "https://{{ proxmox_host }}:8006/api2/json/access/ticket"
method: POST
body_format: form-urlencoded
body:
username: "{{ proxmox_api_user }}"
password: "{{ proxmox_api_password }}"
validate_certs: "{{ proxmox_validate_certs }}"
status_code: 200
register: _proxmox_auth
no_log: true
- name: "deploy-vm | migrate '{{ vm_name }}' : {{ resolved_node }} → {{ proxmox_node }}"
ansible.builtin.uri:
url: >-
https://{{ proxmox_host }}:8006/api2/json/nodes/{{ resolved_node }}/qemu/{{ resolved_vm_id }}/migrate
method: POST
headers:
Cookie: "PVEAuthCookie={{ _proxmox_auth.json.data.ticket }}"
CSRFPreventionToken: "{{ _proxmox_auth.json.data.CSRFPreventionToken }}"
body_format: form-urlencoded
body:
target: "{{ proxmox_node }}"
online: 0
"with-local-disks": "{{ 1 if vm_migrate_with_local_disks | bool else 0 }}"
validate_certs: "{{ proxmox_validate_certs }}"
status_code: 200
register: _migrate_result
# Proxmox retourne un UPID (ex: UPID:node:pid:…) — on poll jusqu'à status = stopped
- name: deploy-vm | wait for migration task to complete
ansible.builtin.uri:
url: >-
https://{{ proxmox_host }}:8006/api2/json/nodes/{{ resolved_node }}/tasks/{{ _migrate_result.json.data | urlencode }}/status
method: GET
headers:
Cookie: "PVEAuthCookie={{ _proxmox_auth.json.data.ticket }}"
validate_certs: "{{ proxmox_validate_certs }}"
register: _migration_status
until: _migration_status.json.data.status == 'stopped'
retries: "{{ (vm_wait_timeout | int / 10) | int }}"
delay: 10
- name: deploy-vm | assert migration succeeded
ansible.builtin.assert:
that: _migration_status.json.data.exitstatus == 'OK'
fail_msg: >-
Migration de '{{ vm_name }}' vers '{{ proxmox_node }}' échouée :
{{ _migration_status.json.data.exitstatus }}
- name: deploy-vm | update resolved_node after migration
ansible.builtin.set_fact:
resolved_node: "{{ proxmox_node }}"
+19
View File
@@ -0,0 +1,19 @@
---
- name: deploy-vm | start VM '{{ vm_name }}'
community.general.proxmox_kvm:
api_host: "{{ proxmox_host }}"
api_user: "{{ proxmox_api_user }}"
api_password: "{{ proxmox_api_password }}"
node: "{{ resolved_node }}"
vmid: "{{ resolved_vm_id }}"
state: started
timeout: "{{ vm_wait_timeout }}"
- name: deploy-vm | deployment summary
ansible.builtin.debug:
msg:
- "VM '{{ vm_name }}' déployée avec succès"
- "VMID : {{ resolved_vm_id }}"
- "IP : {{ vm_ip }}"
- "Node : {{ resolved_node }}"
- "Cœurs : {{ vm_cores }} | RAM : {{ vm_memory }} Mo"