--- - name: Cloner une VM Debian depuis le modele Proxmox hosts: localhost connection: local gather_facts: false become: false vars: # Config Proxmox de l'instance : on cherche dans l'inventaire de l'instance, # quel que soit son nom (lab > principal > production). Compatible « par instance ». setops_inventaires: [lab, principal, production] # Voute UNIQUE de l'instance : all/vault.yml porte TOUS les secrets, jeton # d'hyperviseur compris (D-19). `proxmox.vault.yml` a ete retire le 2026-08-03 : # tolere "en compatibilite", il restait le SEUL porteur du jeton chez un tenant, # et il etait gitignore — une voute unique qui ne l'etait pas, dans un fichier # qui ne voyageait avec aucun depot. P18 ne pouvait pas le voir : il verifie le # gabarit, pas le contenu reel. # `proxmox.local.yml` reste une surcharge locale volontaire, non versionnee. setops_gv_proxmox: [proxmox.yml, proxmox.local.yml, all/vault.yml] # Le CLUSTER appartient a l'HEBERGEUR (API, noeuds, stockages, ponts). Son fichier # vit dans le depot de l'hebergeur, que le symlink `underlay.yml` designe deja # (D-14/D-17) : on en DERIVE le chemin plutot que de redeclarer l'hebergeur. Absent # (depot sans underlay monte), le `stat` echoue simplement et rien n'est charge. setops_proxmox_hebergeur: "{{ (playbook_dir ~ '/../../underlay.yml') | realpath | dirname }}/proxmox-hebergeur.yml" tasks: - name: Verifier les fichiers de variables Proxmox optionnels ansible.builtin.stat: path: "{{ playbook_dir }}/../../instance/inventories/{{ item.0 }}/group_vars/{{ item.1 }}" loop: "{{ setops_inventaires | product(setops_gv_proxmox) | list }}" loop_control: label: "{{ item.0 }}/{{ item.1 }}" register: proxmox_fichiers_variables - name: Charger les variables Proxmox non sensibles disponibles ansible.builtin.include_vars: file: "{{ item.stat.path }}" loop: "{{ proxmox_fichiers_variables.results }}" when: - item.stat.exists - item.stat.path is not search('vault') - name: Localiser le fichier Proxmox de l'hebergeur ansible.builtin.stat: path: "{{ setops_proxmox_hebergeur }}" register: proxmox_fichier_hebergeur - name: Charger le cluster de l'hebergeur (autoritaire sur ses propres cles) ansible.builtin.include_vars: file: "{{ setops_proxmox_hebergeur }}" when: proxmox_fichier_hebergeur.stat.exists - name: Charger les secrets Proxmox Vault block: - name: Charger les secrets de la voute Proxmox ansible.builtin.include_vars: file: "{{ item.stat.path }}" loop: "{{ proxmox_fichiers_variables.results }}" when: - item.stat.exists - item.stat.path is search('vault') no_log: true rescue: - name: Expliquer l echec de chargement du Vault ansible.builtin.fail: msg: >- Impossible de charger la voute Proxmox (group_vars/all/vault.yml ou proxmox.vault.yml). Si elle est chiffree avec Ansible Vault, fournir son mot de passe ou definir ANSIBLE_VAULT_PASSWORD_FILE avant de relancer make creer-vm. - name: Appliquer les valeurs Proxmox avec repli environnement ansible.builtin.set_fact: proxmox_api_host_effectif: "{{ proxmox_api_host | default(lookup('env', 'PROXMOX_API_HOST'), true) }}" proxmox_api_user_effectif: "{{ proxmox_api_user | default(lookup('env', 'PROXMOX_API_USER'), true) }}" # `split('!')[-1]` : proxmoxer recompose lui-meme `utilisateur!nom` a partir de # `api_user` et `api_token_id`. Lui passer la forme COMPLETE produit # `ansible@pve!ansible@pve!nom` et un 401 muet — le meme jeton fonctionne en # HTTP direct, ce qui rend le diagnostic trompeur. Verifie contre le cluster le # 2026-08-03 : forme complete = 401, forme courte = OK. Sans effet si la voute # stocke deja le nom seul, donc les deux ecritures restent acceptees. proxmox_api_token_id_effectif: "{{ (proxmox_api_token_id | default(lookup('env', 'PROXMOX_API_TOKEN_ID'), true)) | string | split('!') | last }}" proxmox_api_token_secret_effectif: "{{ proxmox_api_token_secret | default(lookup('env', 'PROXMOX_API_TOKEN_SECRET'), true) }}" proxmox_api_port_effectif: "{{ proxmox_api_port | default(lookup('env', 'PROXMOX_API_PORT'), true) }}" no_log: true - name: Valider les parametres obligatoires Proxmox ansible.builtin.assert: that: - proxmox_api_host_effectif | length > 0 - proxmox_api_user_effectif | length > 0 - proxmox_api_token_id_effectif | length > 0 - proxmox_api_token_secret_effectif | length > 0 - proxmox_clone_nom is defined - proxmox_clone_nom | length > 0 - proxmox_clone_vmid_modele is defined - proxmox_clone_vmid is defined - proxmox_clone_noeud is defined - proxmox_clone_noeud | length > 0 - proxmox_clone_ipconfig0 is defined - proxmox_clone_ipconfig0 | length > 0 fail_msg: "Parametres Proxmox incomplets. Verifier proxmox.yml, la voute (all/vault.yml ou proxmox.vault.yml) et les variables make propres a la VM." - name: Lire la cle publique SSH depuis le fichier fourni ansible.builtin.set_fact: proxmox_clone_sshkeys: "{{ lookup('ansible.builtin.file', proxmox_clone_cle_publique_fichier) }}" when: - proxmox_clone_cle_publique is not defined - proxmox_clone_cle_publique_fichier is defined - proxmox_clone_cle_publique_fichier | length > 0 - name: Utiliser la cle publique SSH fournie directement ansible.builtin.set_fact: proxmox_clone_sshkeys: "{{ proxmox_clone_cle_publique }}" when: - proxmox_clone_cle_publique is defined - proxmox_clone_cle_publique | length > 0 - name: Construire les serveurs DNS Cloud-Init ansible.builtin.set_fact: proxmox_clone_nameservers: >- {{ (proxmox_clone_dns | default('') | split(',')) | map('trim') | reject('equalto', '') | list }} - name: Verifier la bibliotheque Python proxmoxer locale ansible.builtin.command: cmd: "{{ ansible_playbook_python }} -c 'import proxmoxer'" changed_when: false failed_when: false register: proxmoxer_verification - name: Refuser si proxmoxer est absent ansible.builtin.fail: msg: >- La bibliotheque Python proxmoxer est absente pour {{ ansible_playbook_python }}. Installer le paquet systeme approprie, par exemple sudo apt install python3-proxmoxer, ou installer proxmoxer dans l'environnement Python utilise par Ansible, puis relancer make creer-vm. when: proxmoxer_verification.rc != 0 - name: S'assurer que le pool du tenant existe # Un pool par tenant : la console Proxmox affiche le NOM, et onze serveurs # portent le meme d'un tenant a l'autre. Le pool restitue l'appartenance sans # rien renommer. Il doit exister AVANT le clone — `proxmox_kvm` echoue sur un # pool inconnu, et l'API ne sait pas changer le pool d'une VM deja creee. community.general.proxmox_pool: api_host: "{{ proxmox_api_host_effectif }}" api_port: "{{ proxmox_api_port_effectif | int if proxmox_api_port_effectif | length > 0 else omit }}" api_user: "{{ proxmox_api_user_effectif }}" api_token_id: "{{ proxmox_api_token_id_effectif }}" api_token_secret: "{{ proxmox_api_token_secret_effectif }}" validate_certs: "{{ proxmox_validate_certs | default(false) | bool }}" poolid: "{{ proxmox_clone_pool }}" comment: "Tenant Set-OPS — genere, ne pas renommer a la main" state: present when: proxmox_clone_pool | default('', true) | length > 0 - name: Cloner la VM depuis le modele community.general.proxmox_kvm: api_host: "{{ proxmox_api_host_effectif }}" api_port: "{{ proxmox_api_port_effectif | int if proxmox_api_port_effectif | length > 0 else omit }}" api_user: "{{ proxmox_api_user_effectif }}" api_token_id: "{{ proxmox_api_token_id_effectif }}" api_token_secret: "{{ proxmox_api_token_secret_effectif }}" validate_certs: "{{ proxmox_validate_certs | default(false) | bool }}" node: "{{ proxmox_clone_noeud }}" clone: "{{ proxmox_clone_source_nom | default('modele-debian13') }}" vmid: "{{ proxmox_clone_vmid_modele | int }}" newid: "{{ proxmox_clone_vmid | int }}" name: "{{ proxmox_clone_nom }}" cores: "{{ proxmox_clone_coeurs | int if proxmox_clone_coeurs is defined and proxmox_clone_coeurs | string | length > 0 else omit }}" memory: "{{ proxmox_clone_memoire | int if proxmox_clone_memoire is defined and proxmox_clone_memoire | string | length > 0 else omit }}" storage: "{{ proxmox_clone_stockage | default(omit, true) }}" format: "{{ proxmox_clone_format | default(omit, true) }}" full: "{{ proxmox_clone_complet | default(true) | bool }}" # A la CREATION seulement : l'API ne permet pas de le changer ensuite. pool: "{{ proxmox_clone_pool | default(omit, true) }}" timeout: "{{ proxmox_clone_timeout | default(600) | int }}" # L'API de CLONAGE ignore `cores` et `memory` : le clone herite du gabarit, et le # plan est contredit sans un mot. Mesure du 2026-08-06 : plan 1 coeur / 1024 Mo, # VM creee avec 2 / 2048 — ceux du gabarit. Il faut donc les reposer APRES le clone. - name: Appliquer le gabarit de calcul du plan (le clonage ne le fait pas) community.general.proxmox_kvm: api_host: "{{ proxmox_api_host_effectif }}" api_port: "{{ proxmox_api_port_effectif | int if proxmox_api_port_effectif | length > 0 else omit }}" api_user: "{{ proxmox_api_user_effectif }}" api_token_id: "{{ proxmox_api_token_id_effectif }}" api_token_secret: "{{ proxmox_api_token_secret_effectif }}" validate_certs: "{{ proxmox_validate_certs | default(false) | bool }}" node: "{{ proxmox_clone_noeud }}" vmid: "{{ proxmox_clone_vmid | int }}" cores: "{{ proxmox_clone_coeurs | int }}" memory: "{{ proxmox_clone_memoire | int }}" update: true when: - proxmox_clone_coeurs is defined and proxmox_clone_coeurs | string | length > 0 - proxmox_clone_memoire is defined and proxmox_clone_memoire | string | length > 0 - name: Ajuster le reseau de la VM clonee community.general.proxmox_nic: api_host: "{{ proxmox_api_host_effectif }}" api_port: "{{ proxmox_api_port_effectif | int if proxmox_api_port_effectif | length > 0 else omit }}" api_user: "{{ proxmox_api_user_effectif }}" api_token_id: "{{ proxmox_api_token_id_effectif }}" api_token_secret: "{{ proxmox_api_token_secret_effectif }}" validate_certs: "{{ proxmox_validate_certs | default(false) | bool }}" vmid: "{{ proxmox_clone_vmid | int }}" interface: "{{ proxmox_clone_interface | default('net0') }}" model: virtio bridge: "{{ proxmox_clone_pont }}" tag: "{{ proxmox_clone_vlan | int if proxmox_clone_vlan is defined and proxmox_clone_vlan | string | length > 0 else omit }}" firewall: "{{ proxmox_clone_parefeu_interface | default(false) | bool }}" state: present when: - proxmox_clone_pont is defined - proxmox_clone_pont | length > 0 - name: Agrandir le disque principal du clone (grow-only) community.general.proxmox_disk: api_host: "{{ proxmox_api_host_effectif }}" api_port: "{{ proxmox_api_port_effectif | int if proxmox_api_port_effectif | length > 0 else omit }}" api_user: "{{ proxmox_api_user_effectif }}" api_token_id: "{{ proxmox_api_token_id_effectif }}" api_token_secret: "{{ proxmox_api_token_secret_effectif }}" validate_certs: "{{ proxmox_validate_certs | default(false) | bool }}" vmid: "{{ proxmox_clone_vmid | int }}" disk: "{{ proxmox_clone_disque | default('scsi0') }}" size: "{{ proxmox_clone_taille_disque }}" state: resized register: proxmox_redim_disque # Le disque derive du plan est un MINIMUM. Si le template est deja plus grand, # Proxmox refuse de retrecir : on tolere ce cas (grow-only), la VM garde le # disque du template. Toute autre erreur reste bloquante. failed_when: - proxmox_redim_disque is failed - "'shrinking disks is not supported' not in (proxmox_redim_disque.msg | default(''))" when: - proxmox_clone_taille_disque is defined - proxmox_clone_taille_disque | length > 0 - name: Configurer Cloud-Init sur le clone community.general.proxmox_kvm: api_host: "{{ proxmox_api_host_effectif }}" api_port: "{{ proxmox_api_port_effectif | int if proxmox_api_port_effectif | length > 0 else omit }}" api_user: "{{ proxmox_api_user_effectif }}" api_token_id: "{{ proxmox_api_token_id_effectif }}" api_token_secret: "{{ proxmox_api_token_secret_effectif }}" validate_certs: "{{ proxmox_validate_certs | default(false) | bool }}" node: "{{ proxmox_clone_noeud }}" vmid: "{{ proxmox_clone_vmid | int }}" name: "{{ proxmox_clone_nom }}" update: true ciuser: "{{ proxmox_clone_ciuser | default(omit, true) }}" sshkeys: "{{ proxmox_clone_sshkeys | default(omit) }}" ipconfig: ipconfig0: "{{ proxmox_clone_ipconfig0 }}" nameservers: "{{ proxmox_clone_nameservers if proxmox_clone_nameservers | length > 0 else omit }}" searchdomains: "{{ proxmox_clone_domaines_recherche | default(omit, true) }}" agent: "enabled=1" - name: Demarrer le clone community.general.proxmox_kvm: api_host: "{{ proxmox_api_host_effectif }}" api_port: "{{ proxmox_api_port_effectif | int if proxmox_api_port_effectif | length > 0 else omit }}" api_user: "{{ proxmox_api_user_effectif }}" api_token_id: "{{ proxmox_api_token_id_effectif }}" api_token_secret: "{{ proxmox_api_token_secret_effectif }}" validate_certs: "{{ proxmox_validate_certs | default(false) | bool }}" node: "{{ proxmox_clone_noeud }}" vmid: "{{ proxmox_clone_vmid | int }}" name: "{{ proxmox_clone_nom }}" state: started when: proxmox_clone_demarrer | default(true) | bool - name: Afficher le resume du clone ansible.builtin.debug: msg: - "VM creee: {{ proxmox_clone_nom }} (VMID {{ proxmox_clone_vmid }})" - "Pool: {{ proxmox_clone_pool | default('(aucun)', true) }}" - "Cloud-Init ipconfig0: {{ proxmox_clone_ipconfig0 }}"