You've already forked deploy-vm
big fat commit with everything working !
This commit is contained in:
@@ -1,2 +1,235 @@
|
||||
# deploy-vm
|
||||
|
||||
Rôle Ansible pour déployer des VMs Proxmox depuis un template cloud-init.
|
||||
Supporte le déploiement multi-VM, la migration inter-nœuds, le redimensionnement de disque et l'ajout de disques supplémentaires.
|
||||
|
||||
## Fonctionnement
|
||||
|
||||
1. **asserts** — valide les variables obligatoires et les formats (IP CIDR, taille disque, extra disks)
|
||||
2. **clone** — détecte le nœud du template dans le cluster, vérifie si la VM existe déjà, clone si non (idempotent)
|
||||
3. **migrate** — si `proxmox_node` est différent du nœud du template, migre la VM hors-ligne vers le nœud cible
|
||||
4. **configure** — applique CPU, RAM, sockets, cloud-init (IP/gateway/DNS/SSH), démarrage auto, resize disque et ajout de disques supplémentaires
|
||||
5. **start** — démarre la VM et affiche un résumé
|
||||
|
||||
## Prérequis
|
||||
|
||||
- Ansible >= 2.14
|
||||
- Collection `community.general >= 9.0.0` :
|
||||
```bash
|
||||
ansible-galaxy collection install -r requirements.yml
|
||||
```
|
||||
- Template Proxmox cloud-init existant (Packer ou manuel)
|
||||
- Accès API Proxmox (utilisateur + mot de passe)
|
||||
|
||||
## Variables
|
||||
|
||||
### Obligatoires par VM (dans la liste `vms`)
|
||||
|
||||
| Champ | Description | Exemple |
|
||||
|----------|----------------------------------|----------------------|
|
||||
| `name` | Nom de la VM dans Proxmox | `"srv-web01"` |
|
||||
| `ip` | Adresse IP en notation CIDR | `"192.168.1.100/24"` |
|
||||
|
||||
### Connexion Proxmox (à stocker dans vault)
|
||||
|
||||
| Variable | Description |
|
||||
|------------------------|------------------------------------|
|
||||
| `proxmox_api_user` | Utilisateur Proxmox (`user@realm`) |
|
||||
| `proxmox_api_password` | Mot de passe Proxmox |
|
||||
|
||||
### Infrastructure (globales)
|
||||
|
||||
| Variable | Description | Défaut |
|
||||
|--------------------------|-----------------------------------------------------------------------------|---------|
|
||||
| `proxmox_host` | Adresse de l'API Proxmox | `""` |
|
||||
| `proxmox_storage` | Pool de stockage par défaut pour le clone | `""` |
|
||||
| `proxmox_node` | Nœud cible pour la VM. Vide = même nœud que le template. Surchargeble par VM. | `""` |
|
||||
| `proxmox_validate_certs` | Valider le certificat TLS de l'API | `false` |
|
||||
| `template_name` | Nom du template source (surchargeble par VM) | `""` |
|
||||
|
||||
### Ressources VM (globales, surchargeables par VM)
|
||||
|
||||
| Variable | Description | Défaut |
|
||||
|---------------|----------------------|--------|
|
||||
| `vm_cores` | Nombre de vCPUs | `2` |
|
||||
| `vm_sockets` | Nombre de sockets | `1` |
|
||||
| `vm_memory` | RAM en Mo | `2048` |
|
||||
|
||||
### Réseau (globales, surchargeables par VM)
|
||||
|
||||
| Variable | Description | Défaut |
|
||||
|--------------|--------------------------------|--------------------------------------|
|
||||
| `vm_gateway` | Passerelle par défaut | `"192.168.1.1"` |
|
||||
| `vm_dns` | Liste des serveurs DNS | `["9.9.9.9", "8.8.8.8", "8.8.4.4"]` |
|
||||
|
||||
### Disque principal (globales, surchargeables par VM)
|
||||
|
||||
| Variable | Description | Défaut |
|
||||
|----------------|-----------------------------------------------------------------------------|----------|
|
||||
| `vm_disk_name` | Identifiant du disque à redimensionner | `"scsi0"` |
|
||||
| `vm_disk_size` | Taille cible. Vide = pas de resize. Format : `"50G"`, `"500M"`, `"+10G"` | `""` |
|
||||
|
||||
> **Note** : `vm_disk_size` avec préfixe `+` agrandit le disque existant. Sans préfixe, définit la taille absolue. Proxmox **ne réduit pas** les disques.
|
||||
|
||||
### Disques supplémentaires (surchargeables par VM)
|
||||
|
||||
| Variable | Description | Défaut |
|
||||
|-----------------|---------------------------------------------------------------|--------|
|
||||
| `vm_extra_disks`| Liste de disques supplémentaires à créer | `[]` |
|
||||
|
||||
Champs de chaque disque :
|
||||
|
||||
| Champ | Obligatoire | Description | Exemple |
|
||||
|-------------|:-----------:|-------------------------------------------------|---------------|
|
||||
| `disk` | ✓ | Slot Proxmox (`scsi1`, `virtio1`, `sata1`…) | `"scsi1"` |
|
||||
| `size` | ✓ | Taille absolue (pas de `+`). Format : `NUNité` | `"50G"` |
|
||||
| `storage` | | Pool de stockage (défaut : `proxmox_storage`) | `"datavm"` |
|
||||
| `ssd` | | Émuler un SSD | `true` |
|
||||
| `iothread` | | Activer iothread | `true` |
|
||||
| `backup` | | Inclure dans les sauvegardes | `true` |
|
||||
| `cache` | | Mode de cache (`writeback`, `none`…) | `"writeback"` |
|
||||
|
||||
### Cloud-init (globales, surchargeables par VM)
|
||||
|
||||
| Variable | Description | Défaut |
|
||||
|-----------------|------------------------------------------------------|--------|
|
||||
| `vm_ciuser` | Utilisateur créé par cloud-init | `""` |
|
||||
| `vm_cipassword` | Mot de passe (préférer `vm_sshkeys`, stocker en vault) | `""` |
|
||||
| `vm_sshkeys` | Clés SSH publiques (séparées par `\n`) | `""` |
|
||||
|
||||
### Options de déploiement (globales, surchargeables par VM)
|
||||
|
||||
| Variable | Description | Défaut |
|
||||
|-------------------------------|------------------------------------------------------------------|---------|
|
||||
| `vm_full_clone` | Clone complet (`true`) ou lié (`false`) | `true` |
|
||||
| `vm_start_on_boot` | Démarrage automatique avec le nœud Proxmox | `true` |
|
||||
| `vm_wait_timeout` | Timeout pour le clone et le démarrage (secondes) | `300` |
|
||||
| `vm_force_update` | Autoriser la reconfiguration d'une VM existante | `false` |
|
||||
| `vm_migrate_with_local_disks` | Migrer les disques locaux (nécessaire si stockage non partagé) | `true` |
|
||||
|
||||
## Utilisation
|
||||
|
||||
### 1. Installer les dépendances
|
||||
|
||||
```bash
|
||||
ansible-galaxy collection install -r requirements.yml
|
||||
```
|
||||
|
||||
### 2. Déclarer les VMs dans les variables du play
|
||||
|
||||
```yaml
|
||||
# group_vars/proxmox.yml (ou host_vars, ou directement dans le playbook)
|
||||
|
||||
proxmox_host: "192.168.1.4"
|
||||
proxmox_storage: "datavm"
|
||||
proxmox_node: "proxmox01" # nœud par défaut pour toutes les VMs
|
||||
template_name: "template-debian-12"
|
||||
|
||||
vms:
|
||||
- name: srv-web01
|
||||
ip: "192.168.1.101/24"
|
||||
cores: 2
|
||||
memory: 2048
|
||||
disk_size: "30G"
|
||||
|
||||
- name: srv-db01
|
||||
ip: "192.168.1.110/24"
|
||||
cores: 4
|
||||
memory: 8192
|
||||
disk_size: "50G"
|
||||
extra_disks:
|
||||
- disk: scsi1
|
||||
size: "100G"
|
||||
storage: "datavm"
|
||||
ssd: true
|
||||
backup: true
|
||||
|
||||
- name: srv-app01
|
||||
ip: "192.168.1.120/24"
|
||||
proxmox_node: "proxmox02" # nœud différent → migration automatique
|
||||
cores: 4
|
||||
memory: 4096
|
||||
disk_size: "30G"
|
||||
```
|
||||
|
||||
### 3. Appeler le rôle dans un playbook
|
||||
|
||||
```yaml
|
||||
- hosts: proxmox
|
||||
gather_facts: false
|
||||
vars_files:
|
||||
- vault.yml
|
||||
roles:
|
||||
- role: brainsys.deploy_vm
|
||||
tags: deploy_vm
|
||||
```
|
||||
|
||||
### 4. Lancer le déploiement
|
||||
|
||||
```bash
|
||||
ansible-playbook playbooks/proxmox.yml \
|
||||
-l proxmox01.homelab.example.com \
|
||||
-t deploy_vm \
|
||||
--ask-vault-pass
|
||||
```
|
||||
|
||||
### 5. Reconfigurer une VM existante
|
||||
|
||||
Par défaut le rôle échoue si la VM existe déjà (protection contre les modifications accidentelles).
|
||||
Pour autoriser la reconfiguration :
|
||||
|
||||
```bash
|
||||
ansible-playbook playbooks/proxmox.yml \
|
||||
-l proxmox01.homelab.example.com \
|
||||
-t deploy_vm \
|
||||
--ask-vault-pass \
|
||||
-e vm_force_update=true
|
||||
```
|
||||
|
||||
Ou par VM dans la liste :
|
||||
```yaml
|
||||
vms:
|
||||
- name: srv-web01
|
||||
ip: "192.168.1.101/24"
|
||||
force_update: true
|
||||
```
|
||||
|
||||
## Champs disponibles dans la liste `vms`
|
||||
|
||||
Chaque item de la liste `vms` peut surcharger n'importe quelle variable globale :
|
||||
|
||||
| Champ | Variable rôle correspondante |
|
||||
|-----------------------------|-----------------------------------|
|
||||
| `name` | `vm_name` *(obligatoire)* |
|
||||
| `ip` | `vm_ip` *(obligatoire)* |
|
||||
| `id` | `vm_id` (auto si absent) |
|
||||
| `proxmox_node` | `proxmox_node` |
|
||||
| `template_name` | `template_name` |
|
||||
| `cores` | `vm_cores` |
|
||||
| `sockets` | `vm_sockets` |
|
||||
| `memory` | `vm_memory` |
|
||||
| `gateway` | `vm_gateway` |
|
||||
| `dns` | `vm_dns` |
|
||||
| `disk_name` | `vm_disk_name` |
|
||||
| `disk_size` | `vm_disk_size` |
|
||||
| `extra_disks` | `vm_extra_disks` |
|
||||
| `ciuser` | `vm_ciuser` |
|
||||
| `cipassword` | `vm_cipassword` |
|
||||
| `sshkeys` | `vm_sshkeys` |
|
||||
| `full_clone` | `vm_full_clone` |
|
||||
| `start_on_boot` | `vm_start_on_boot` |
|
||||
| `wait_timeout` | `vm_wait_timeout` |
|
||||
| `force_update` | `vm_force_update` |
|
||||
| `migrate_with_local_disks` | `vm_migrate_with_local_disks` |
|
||||
|
||||
## Sécurité
|
||||
|
||||
- Chiffrer `vault.yml` avec `ansible-vault encrypt vault.yml` — ne jamais le committer en clair.
|
||||
- Préférer les clés SSH (`vm_sshkeys`) aux mots de passe cloud-init (`vm_cipassword`).
|
||||
- Créer un utilisateur Proxmox dédié avec les permissions minimales plutôt que d'utiliser `root@pam`.
|
||||
- Ajouter `vault.yml` au `.gitignore` si le fichier n'est pas chiffré.
|
||||
|
||||
## Licence
|
||||
|
||||
MIT
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
---
|
||||
proxmox_host: ""
|
||||
proxmox_storage: ""
|
||||
proxmox_validate_certs: false
|
||||
|
||||
# Nœud cible pour la VM finale.
|
||||
# Si différent du nœud du template, une migration offline est effectuée après le clone.
|
||||
# Laisser vide pour que la VM reste sur le même nœud que le template.
|
||||
proxmox_node: ""
|
||||
|
||||
template_name: ""
|
||||
|
||||
# Migration
|
||||
vm_migrate_with_local_disks: true # Nécessaire si le stockage n'est pas partagé (NFS/Ceph)
|
||||
|
||||
vm_gateway: "192.168.1.1"
|
||||
vm_dns:
|
||||
- "9.9.9.9"
|
||||
- "8.8.8.8"
|
||||
- "8.8.4.4"
|
||||
|
||||
vm_cores: 2
|
||||
vm_sockets: 1
|
||||
vm_memory: 2048 # Mo
|
||||
|
||||
vm_disk_name: "scsi0"
|
||||
vm_disk_size: ""
|
||||
|
||||
# Disques supplémentaires (liste, vide par défaut)
|
||||
# Champs obligatoires : disk, size
|
||||
# Champs optionnels : storage (défaut: proxmox_storage), cache, format, ssd, iothread, backup
|
||||
# Exemple :
|
||||
# vm_extra_disks:
|
||||
# - disk: scsi1
|
||||
# size: 100G
|
||||
# - disk: scsi2
|
||||
# size: 50G
|
||||
# storage: backup-pool
|
||||
# cache: writeback
|
||||
# ssd: true
|
||||
# backup: true
|
||||
vm_extra_disks: []
|
||||
|
||||
vm_ciuser: "" # Nom d'utilisateur cloud-init
|
||||
vm_cipassword: "" # Mot de passe (préférer vm_sshkeys) — à stocker dans vault
|
||||
vm_sshkeys: "" # Clés SSH publiques (séparées par \n)
|
||||
|
||||
vm_full_clone: true
|
||||
vm_start_on_boot: true
|
||||
vm_wait_timeout: 300
|
||||
|
||||
# Mettre à true pour autoriser la mise à jour d'une VM existante
|
||||
vm_force_update: false
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
galaxy_info:
|
||||
role_name: deploy_vm
|
||||
author: brainsys
|
||||
description: Déploiement idempotent de VMs Proxmox depuis un template via cloud-init
|
||||
license: MIT
|
||||
min_ansible_version: "2.14"
|
||||
platforms:
|
||||
- name: proxmox
|
||||
versions:
|
||||
- "7"
|
||||
- "8"
|
||||
- "9"
|
||||
galaxy_tags:
|
||||
- proxmox
|
||||
- virtualization
|
||||
- vm
|
||||
- cloud_init
|
||||
|
||||
dependencies: []
|
||||
@@ -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)
|
||||
@@ -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
|
||||
}}
|
||||
@@ -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({}))
|
||||
@@ -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
|
||||
@@ -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 }}"
|
||||
@@ -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 }}"
|
||||
@@ -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"
|
||||
Reference in New Issue
Block a user