From 3276afa34cec92ae53fc459117d4fc3be09ab18a Mon Sep 17 00:00:00 2001 From: Ludovic Cartier Date: Fri, 29 May 2026 18:30:16 +0200 Subject: [PATCH] big fat commit with everything working ! --- README.md | 233 +++++++++++++++++++++++++++++++++++++++++++ defaults/main.yml | 53 ++++++++++ meta/main.yml | 20 ++++ tasks/asserts.yml | 82 +++++++++++++++ tasks/clone.yml | 48 +++++++++ tasks/configure.yml | 88 ++++++++++++++++ tasks/deploy_one.yml | 11 ++ tasks/main.yml | 55 ++++++++++ tasks/migrate.yml | 65 ++++++++++++ tasks/start.yml | 19 ++++ 10 files changed, 674 insertions(+) create mode 100644 defaults/main.yml create mode 100644 meta/main.yml create mode 100644 tasks/asserts.yml create mode 100644 tasks/clone.yml create mode 100644 tasks/configure.yml create mode 100644 tasks/deploy_one.yml create mode 100644 tasks/main.yml create mode 100644 tasks/migrate.yml create mode 100644 tasks/start.yml diff --git a/README.md b/README.md index 362c092..743a93a 100644 --- a/README.md +++ b/README.md @@ -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 + diff --git a/defaults/main.yml b/defaults/main.yml new file mode 100644 index 0000000..c06ec51 --- /dev/null +++ b/defaults/main.yml @@ -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 \ No newline at end of file diff --git a/meta/main.yml b/meta/main.yml new file mode 100644 index 0000000..d2c789d --- /dev/null +++ b/meta/main.yml @@ -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: [] diff --git a/tasks/asserts.yml b/tasks/asserts.yml new file mode 100644 index 0000000..f724043 --- /dev/null +++ b/tasks/asserts.yml @@ -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) \ No newline at end of file diff --git a/tasks/clone.yml b/tasks/clone.yml new file mode 100644 index 0000000..9427ab8 --- /dev/null +++ b/tasks/clone.yml @@ -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 + }} diff --git a/tasks/configure.yml b/tasks/configure.yml new file mode 100644 index 0000000..6d781be --- /dev/null +++ b/tasks/configure.yml @@ -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({})) diff --git a/tasks/deploy_one.yml b/tasks/deploy_one.yml new file mode 100644 index 0000000..b866a9b --- /dev/null +++ b/tasks/deploy_one.yml @@ -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 diff --git a/tasks/main.yml b/tasks/main.yml new file mode 100644 index 0000000..d74d32d --- /dev/null +++ b/tasks/main.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 }}" \ No newline at end of file diff --git a/tasks/migrate.yml b/tasks/migrate.yml new file mode 100644 index 0000000..e25b3af --- /dev/null +++ b/tasks/migrate.yml @@ -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 }}" diff --git a/tasks/start.yml b/tasks/start.yml new file mode 100644 index 0000000..fbe7b5b --- /dev/null +++ b/tasks/start.yml @@ -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"