--- - 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" # LA VOUTE DE L'UNDERLAY — les secrets du MONDE PHYSIQUE (jeton d'API du cluster). # Meme derivation que le fichier ci-dessus : le symlink `underlay.yml` designe deja # l'hebergeur, on n'a rien a redeclarer. Depuis le 2026-08-22 ces secrets ne vivent # plus dans la voute d'un tenant : recopies chez chacun, ils ne pouvaient plus etre # revoques isolement (docs/frontiere-physique-virtuel.md). setops_voute_underlay: "{{ (playbook_dir ~ '/../../underlay.yml') | realpath | dirname }}/underlay.vault.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: Verifier la voute de l'underlay ansible.builtin.stat: path: "{{ setops_voute_underlay }}" register: proxmox_voute_underlay - 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 # APRES celle du tenant, donc elle GAGNE : quand les deux portent la cle, c'est # celle de l'hebergeur qui fait foi. C'est le sens de la frontiere entre les deux # mondes, et ca rend la migration d'un site sans coupure. - name: Charger les secrets du monde physique (voute de l'underlay) ansible.builtin.include_vars: file: "{{ setops_voute_underlay }}" when: proxmox_voute_underlay.stat.exists 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 # LA CLE D'AMORCAGE S'AJOUTE, ELLE NE REMPLACE PAS. # # L'exploitant doit garder son acces a la machine qu'il vient de creer : ecraser sa # cle par celle du runner rendrait le runner seul maitre du terrain, ce qui est # exactement l'inverse de la doctrine (c'est l'humain qui arme, cf. # docs/filiation-emancipation.md). Deux cles, deux lignes, et cloud-init les pose # toutes les deux. # # Elle n'arrive ICI que pour le runner d'un tenant : `inventory_host.py` la rend vide # partout ailleurs, et cette tache ne se declenche donc pas. # # DEUX PIEGES, MESURES LE 2026-08-28, ET LE MEME MESSAGE POUR LES DEUX : # 500 Internal Server Error: SSH public key validation error # # 1. LE SEPARATEUR. Un scalaire PLIE (`>-`) ne produit pas de saut de ligne : `'\n'` # y reste deux caracteres, une barre et un « n ». Seul le scalaire YAML entre # GUILLEMETS le convertit avant que Jinja ne le lise (verifie en base64 sur les # trois formes candidates). # # 2. LA LISTE PEUT N'AVOIR QU'UN ELEMENT — et c'est le cas courant. Sur cette flotte, # `CLE_SSH_PUBLIQUE` est vide : la cle de l'exploitant vient du GABARIT DORE, pas # de cloud-init. Une simple concatenation rendait donc une valeur commencant par # un saut de ligne, que Proxmox refuse tout autant. On assemble une LISTE et on # laisse `select` retirer les vides — la forme est juste qu'il y ait zero, une ou # deux cles. - name: Ajouter la cle d'amorcage du runner de SITE (insemination) ansible.builtin.set_fact: proxmox_clone_sshkeys: "{{ [proxmox_clone_sshkeys | default('') | trim, proxmox_clone_cles_amorcage | trim] | select | join('\n') }}" when: - proxmox_clone_cles_amorcage is defined - proxmox_clone_cles_amorcage | 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 # LE CLONE SE CIBLE PAR VMID, JAMAIS PAR NOM — et c'est tout l'objet de ces trois # taches. `community.general.proxmox_kvm` cherche d'abord une VM portant le `name` # demande ; s'il en trouve une, il conclut « elle existe deja », rend `ok` et ne # clone RIEN. Aucune tache n'apparait meme cote cluster. # # Or les noms courts sont VOLONTAIREMENT identiques d'un tenant a l'autre (meme # fonction, meme nom) : c'est le pool qui restitue l'appartenance. Le premier clone # de Technolibre — `backup-01` — est donc tombe sur le `backup-01` de Chezlepro et # n'a rien fait, puis l'attente a expire sur une configuration qui n'existerait # jamais. Mesure du 2026-08-10 : zero VM creee, zero tache `qmclone` au cluster. # # Le defaut etait invisible tant qu'un seul tenant existait. C'est exactement ce # qu'une epreuve de portabilite doit trouver. - name: Recenser les VM du cluster (pour cibler par VMID) vars: proxmox_api_racine: "https://{{ proxmox_api_host_effectif }}:{{ proxmox_api_port_effectif | default(8006, true) }}" ansible.builtin.uri: url: "{{ proxmox_api_racine }}/api2/json/cluster/resources?type=vm" method: GET headers: Authorization: >- PVEAPIToken={{ proxmox_api_user_effectif }}!{{ proxmox_api_token_id_effectif }}={{ proxmox_api_token_secret_effectif }} validate_certs: "{{ proxmox_validate_certs | default(false) | bool }}" status_code: 200 register: proxmox_inventaire_vm changed_when: false no_log: true - name: Retenir si le VMID cible est deja pris ansible.builtin.set_fact: proxmox_clone_deja_la: >- {{ (proxmox_inventaire_vm.json.data | default([])) | selectattr('vmid', 'defined') | selectattr('vmid', 'equalto', proxmox_clone_vmid | int) | list | length > 0 }} # OU VIT LE GABARIT — et ce n'est pas forcement ou va la VM (2026-08-25). # # L'URL de clonage visait `nodes/{{ proxmox_clone_noeud }}`, c'est-a-dire le noeud de # DESTINATION. Or l'API veut le noeud qui DETIENT le modele ; la destination se dit # par `target`. Cloner vers gandalf un gabarit qui vit sur asgard rendait donc : # # 500 Configuration file 'nodes/gandalf/qemu-server/99998.conf' does not exist # # Ca n'etait jamais apparu parce que toutes les VM naissaient sur le noeud du gabarit # et migraient ensuite. La premiere machine placee ailleurs l'a revele. - name: Localiser le noeud qui detient le gabarit ansible.builtin.set_fact: proxmox_noeud_gabarit: >- {{ ((proxmox_inventaire_vm.json.data | default([])) | selectattr('vmid', 'defined') | selectattr('vmid', 'equalto', proxmox_clone_vmid_modele | int) | map(attribute='node') | list | first) | default('', true) }} - name: "Un noeud de la grappe detient bien le gabarit {{ proxmox_clone_vmid_modele }}" ansible.builtin.assert: that: proxmox_noeud_gabarit | length > 0 fail_msg: >- Aucun noeud de la grappe ne detient le gabarit {{ proxmox_clone_vmid_modele }}. Sans lui, rien a cloner. - name: Cloner la VM depuis le modele (API directe, cible par VMID) vars: proxmox_api_racine: "https://{{ proxmox_api_host_effectif }}:{{ proxmox_api_port_effectif | default(8006, true) }}" ansible.builtin.uri: url: "{{ proxmox_api_racine }}/api2/json/nodes/{{ proxmox_noeud_gabarit }}/qemu/{{ proxmox_clone_vmid_modele | int }}/clone" method: POST headers: Authorization: >- PVEAPIToken={{ proxmox_api_user_effectif }}!{{ proxmox_api_token_id_effectif }}={{ proxmox_api_token_secret_effectif }} body_format: form-urlencoded # Corps ecrit en MAPPING YAML, pas assemble en Jinja : une expression `>-` rend # une CHAINE, et le premier jet a ainsi perdu le `pool` en route — la VM est # nee hors de son pool, sans un mot. `omit` retire proprement les cles vides. body: newid: "{{ proxmox_clone_vmid | int }}" name: "{{ proxmox_clone_nom }}" full: "{{ 1 if (proxmox_clone_complet | default(true) | bool) else 0 }}" storage: "{{ proxmox_clone_stockage | default(omit, true) }}" format: "{{ proxmox_clone_format | default(omit, true) }}" # A la CREATION seulement : l'API ne sait pas changer le pool d'une VM # existante par cet appel (il faut passer par le membre du pool). pool: "{{ proxmox_clone_pool | default(omit, true) }}" # LA DESTINATION SE DIT ICI, pas dans l'URL. Omise quand elle coincide avec le # noeud du gabarit : l'API refuse un `target` inutile sur stockage non partage. target: "{{ proxmox_clone_noeud if proxmox_clone_noeud != proxmox_noeud_gabarit else omit }}" validate_certs: "{{ proxmox_validate_certs | default(false) | bool }}" status_code: 200 register: proxmox_clone_lance # Le corps ne porte aucun secret, mais l'en-tete si : `no_log` masquerait alors # l'erreur de l'API, qui est justement ce qu'on veut lire. On ne journalise donc # que l'ECHEC, et sans les en-tetes. failed_when: false no_log: true when: not proxmox_clone_deja_la # L'ECHEC DU POST ETAIT AVALE. `failed_when: false` protege du 500 « VMID deja pris », # mais il avalait AUSSI un refus reel — et la tache d'attente qui suit se bloquait # alors sur un clonage qui n'avait jamais commence. Un blocage sans message, la panne # la plus chere a diagnostiquer. On garde le masque pour l'en-tete, et on releve # l'echec ici, message de l'API compris. - name: "Le clonage a bien ete accepte par l'API" ansible.builtin.assert: that: (proxmox_clone_lance.status | default(0)) == 200 fail_msg: >- L'API a refuse le clonage ({{ proxmox_clone_lance.status | default('aucune reponse') }}) : {{ proxmox_clone_lance.json.message | default(proxmox_clone_lance.msg | default('sans message')) }} — gabarit {{ proxmox_clone_vmid_modele }} sur « {{ proxmox_noeud_gabarit }} », destination « {{ proxmox_clone_noeud }} ». when: not proxmox_clone_deja_la # ATTENDRE QUE LE CLONE SOIT REELLEMENT FINI, pas seulement demande. # # `POST .../clone` rend un UPID et la main IMMEDIATEMENT : Proxmox copie le disque en # tache de fond. `proxmox_kvm`, qu'on a remplace, attendait la fin (`timeout: 600`) — la # reecriture par API avait perdu cette attente sans que ca se voie, parce qu'en # sequentiel l'attente de SSH qui suit l'absorbait. # # En PARALLELE, c'est tout autre chose : `make creer-vm` rend la main pendant que le # disque se copie, la limite de concurrence ne retient plus que des processus vides, et # les clones s'empilent. Mesure du 2026-08-10 : limite a 4, DOUZE copies integrales du # gabarit simultanees sur le meme stockage. Le CPU de l'hyperviseur a 2 %, et pourtant # tout ramait — ce n'etait pas la machine, c'etait TrueNAS. - name: Attendre la fin reelle du clonage vars: proxmox_api_racine: "https://{{ proxmox_api_host_effectif }}:{{ proxmox_api_port_effectif | default(8006, true) }}" ansible.builtin.uri: # LE NOEUD VIENT DE L'UPID LUI-MEME, et de nulle part ailleurs (2026-08-25). # # Cette attente interrogeait `nodes/{{ proxmox_clone_noeud }}` — la DESTINATION. # Or un clonage inter-noeuds s'execute sur le noeud du GABARIT : l'UPID n'existait # pas la ou on le cherchait, l'API repondait en erreur, `failed_when: false` # l'avalait, et `until` n'etait jamais satisfait. Resultat : soixante tentatives a # dix secondes pour un clonage deja termine en 87 s — dix minutes d'attente # immobile, puis la suite qui reprend sans un mot. # # Un UPID s'ecrit `UPID:::...`. Le noeud y est deja ; le redemander a # une variable, c'est se donner une chance de se tromper. url: >- {{ proxmox_api_racine }}/api2/json/nodes/{{ proxmox_clone_lance.json.data.split(':')[1] }}/tasks/{{ proxmox_clone_lance.json.data | urlencode }}/status method: GET headers: Authorization: >- PVEAPIToken={{ proxmox_api_user_effectif }}!{{ proxmox_api_token_id_effectif }}={{ proxmox_api_token_secret_effectif }} validate_certs: "{{ proxmox_validate_certs | default(false) | bool }}" status_code: [200] register: proxmox_clone_tache until: (proxmox_clone_tache.json.data.status | default('running')) == 'stopped' retries: "{{ ((proxmox_clone_timeout | default(600) | int) / 10) | round | int }}" delay: 10 changed_when: false failed_when: false no_log: true when: - not proxmox_clone_deja_la - proxmox_clone_lance.json.data is defined # UN AVERTISSEMENT N'EST PAS UN ECHEC — ET IL NE DOIT PAS DISPARAITRE NON PLUS. # # Proxmox rend trois formes d'`exitstatus` : `OK`, `WARNINGS: ` pour une tache # QUI A ABOUTI en signalant quelque chose, et un texte d'erreur pour un vrai echec. # La garde n'acceptait que `OK` : elle declarait donc perdus des clonages termines. # # MESURE DU 2026-09-01, RECONSTRUCTION DE CHEZLEPRO. Cinq VM sur quinze refusees, # toutes avec `WARNINGS: 1`. Le journal de la tache disait : # # transferred 16.0 GiB of 16.0 GiB (100.00%) # can't deactivate LV 'vm-9006-disk-1': Logical volume in use. # WARN: volume deactivation failed # # Le disque etait transfere INTEGRALEMENT. Proxmox essaie ensuite de desactiver le # volume DU GABARIT, et n'y parvient pas tant qu'un autre clonage parallele s'en # sert. C'est la consequence NORMALE de cloner un meme gabarit en parallele — la # condition meme que `flotte-creer` recherche pour aller vite. # # Ce que ca coutait : la reconstruction s'arretait sur cinq VM parfaitement clonees, # et le message parlait d'« etat stopped » — l'etat de la TACHE, qui signifie # « terminee ». On lit « la VM est arretee » et on cherche un probleme de demarrage # qui n'existe pas. Le message dit desormais de quoi il parle. - name: Le clonage a-t-il signale quelque chose en aboutissant ? ansible.builtin.debug: msg: >- Clonage de {{ proxmox_clone_nom }} abouti AVEC AVERTISSEMENT ({{ proxmox_clone_tache.json.data.exitstatus }}). Le plus courant est « volume deactivation failed » sur le disque du gabarit : un autre clonage parallele s'en servait encore. Le journal complet de la tache est sur « {{ proxmox_clone_lance.json.data.split(':')[1] | default('?') }} ». when: - not proxmox_clone_deja_la - (proxmox_clone_tache.json.data.status | default('')) == 'stopped' - (proxmox_clone_tache.json.data.exitstatus | default('')) is match('WARNINGS') - name: Dire si le clonage ne s'est pas termine correctement ansible.builtin.fail: msg: >- Le clonage de {{ proxmox_clone_nom }} (VMID {{ proxmox_clone_vmid }}) ne s'est pas termine correctement : la TACHE est « {{ proxmox_clone_tache.json.data.status | default('?') }} » (« stopped » = terminee, ce n'est pas l'etat de la VM) et sa sortie est « {{ proxmox_clone_tache.json.data.exitstatus | default('?') }} ». Regarder la tache sur « {{ proxmox_clone_lance.json.data.split(':')[1] | default('?') }} ». when: - not proxmox_clone_deja_la # `exitstatus | default('OK')` faisait passer pour un SUCCES une attente qui # n'avait jamais rien observe : quand l'API repondait en erreur — le cas du # clonage inter-noeuds interroge sur le mauvais noeud — il n'y avait aucun # `json.data`, donc aucun `exitstatus`, donc « OK » par defaut. On exige # desormais d'avoir VU la tache s'arreter, et bien s'arreter. # `OK` et `WARNINGS: ` disent tous deux que la tache a ABOUTI. Tout autre # texte est un vrai echec, et l'absence de sortie signifie qu'on n'a jamais vu # la tache s'arreter — le cas que la note ci-dessus decrit. - >- (proxmox_clone_tache.json.data.status | default('')) != 'stopped' or not ((proxmox_clone_tache.json.data.exitstatus | default('')) == 'OK' or (proxmox_clone_tache.json.data.exitstatus | default('')) is match('WARNINGS')) - name: Dire pourquoi le clonage a echoue, s'il a echoue ansible.builtin.fail: msg: >- Le clonage de {{ proxmox_clone_nom }} (VMID {{ proxmox_clone_vmid }}) depuis le gabarit {{ proxmox_clone_vmid_modele }} a ete refuse par l'API Proxmox (HTTP {{ proxmox_clone_lance.status | default('?') }}). Verifier que le gabarit existe sur {{ proxmox_clone_noeud }}, que le pool {{ proxmox_clone_pool | default('(aucun)', true) }} existe, et que le VMID cible est libre. when: - not proxmox_clone_deja_la - proxmox_clone_lance.status | default(0) != 200 # 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. # Le clone COMPLET rend la main avant que sa configuration ne soit ecrite : sur # un stockage lent, `qm clone` retourne, et le `.conf` n'existe pas encore. La # tache suivante echouait alors sur « Configuration file ... does not exist ». # Observe deux fois sur quinze clones (2026-08-07) — une course, pas un hasard. - name: Attendre que la configuration du clone existe vars: # Assemblee dans une variable : un `>-` replie les retours en ESPACES, ce qui # inserait une espace au milieu de l'URL. Le lint passait, la requete non. proxmox_api_racine: "https://{{ proxmox_api_host_effectif }}:{{ proxmox_api_port_effectif | default(8006, true) }}" ansible.builtin.uri: url: "{{ proxmox_api_racine }}/api2/json/nodes/{{ proxmox_clone_noeud }}/qemu/{{ proxmox_clone_vmid | int }}/config" method: GET headers: Authorization: >- PVEAPIToken={{ proxmox_api_user_effectif }}!{{ proxmox_api_token_id_effectif }}={{ proxmox_api_token_secret_effectif }} validate_certs: "{{ proxmox_validate_certs | default(false) | bool }}" status_code: 200 register: proxmox_clone_conf until: proxmox_clone_conf.status == 200 retries: 30 delay: 5 changed_when: false failed_when: false no_log: true # `no_log` masque la cause au moment ou on en a besoin : le 2026-08-10, l'echec de # cette attente s'est lu « the output has been hidden », et il a fallu interroger le # cluster a la main pour comprendre que le clone n'avait jamais eu lieu. On rend # donc le verdict lisible ici, sans reveler l'en-tete d'autorisation. - name: Dire ce que l'attente du clone a constate ansible.builtin.fail: msg: >- La configuration de {{ proxmox_clone_nom }} (VMID {{ proxmox_clone_vmid }}) n'existe toujours pas apres {{ 30 * 5 }} s sur {{ proxmox_clone_noeud }}. Le clonage n'a donc pas abouti — verifier les taches du cluster (`/cluster/tasks`) : si aucune tache de clonage n'apparait, c'est que rien n'a ete demande a l'API. when: proxmox_clone_conf.status | default(0) != 200 - 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 }}" # LE PLANCHER DE MEMOIRE — ce que l'hote peut REPRENDRE, pas ce qu'il donne. # # `balloon` vaut 0 sur toute la flotte, c'est-a-dire ballooning DESACTIVE : l'hote # ne peut jamais reprendre une page, meme jamais touchee. Mesure : 40 Go alloues # a un ecosysteme pour 15,5 Go reellement utilises — 39 %. Les 24,5 Go restants # sont tenus sans etre lus. # # CE QUE LE BALLOONING N'EST PAS : une compression. L'invite voit `memory` au # demarrage et s'y dimensionne — `shared_buffers` de PostgreSQL, tas d'une JVM, # series en memoire d'un collecteur. Reprendre en dessous de ce qu'il a reserve # le fait swapper A L'INTERIEUR, ce qui coute plus que la RAM gagnee. # # D'ou un PLANCHER DECLARE PAR MACHINE plutot qu'un ratio global : les machines # qui se dimensionnent au demarrage gardent un plancher haut, les autres non. balloon: "{{ proxmox_clone_memoire_min | int }}" update: true # Le noeud vient de finir un clone COMPLET : son API repond encore lentement, et # la lecture expire a 5 s. Mesure du 2026-08-10 : « Read timed out » sur le tout # premier clone, VM creee mais laissee aux valeurs du gabarit — 2 coeurs / 2 Go au # lieu du plan. Une VM sous-dimensionnee en silence est pire qu'un echec franc. register: proxmox_gabarit_calcul until: proxmox_gabarit_calcul is succeeded retries: 6 delay: 10 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 - proxmox_clone_memoire_min is defined and proxmox_clone_memoire_min | string | length > 0 # Un desarmement doit se VOIR. Sans ce message, l'ecart entre ce que l'ecosysteme # declare et ce que la VM recoit resterait muet — et c'est precisement le genre de # silence qui a coute une soiree de diagnostic le 2026-08-25. - name: Dire que le pare-feu Proxmox reste desarme (pont classique, pas un VNet SDN) ansible.builtin.debug: msg: >- `proxmox_clone_parefeu_interface` vaut vrai, mais {{ proxmox_clone_nom }} nait sur `{{ proxmox_clone_pont }}` — un pont Linux, pas un VNet SDN. Le pare-feu Proxmox y jetterait le retour des flux en epingle (`ctstate INVALID`) : il reste desarme. La frontiere continue de policer ce segment. when: - proxmox_clone_parefeu_interface | default(false) | bool - proxmox_clone_pont is defined - proxmox_clone_pont is match('^vmbr[0-9]+$') - 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 }}" # LE PARE-FEU PROXMOX NE S'ARME QUE SUR UN VNET SDN (2026-08-25). # # `firewall=1` fait passer tout le trafic ponte par conntrack. Sur un VNET SDN # c'est sans consequence : en EVPN le routage inter-VNet se fait dans le VRF, SUR # LE NOEUD. Le flux ne quitte jamais l'hyperviseur, conntrack en voit un chemin # coherent, et le devis est-ouest (`devis_proxmox_fw.py`) peut le policer. # # Sur un PONT CLASSIQUE route par une frontiere externe, le meme reglage devient # un piege. Deux VM du meme noeud dans deux VLAN differents ne se parlent qu'en # EPINGLE : la trame sort par le lien physique, la frontiere la route, et elle # revient sur LE MEME PONT. La meme table conntrack voit alors les deux moities de # la connexion, classe le retour `INVALID`, et `PVEFW-FORWARD` le jette. # # MESURE DU 2026-08-25, sur le site decoupe en quatre zones : `site-ops-01` et # `site-pki-01` (asgard), `site-dns-01` et `site-cache-01` (gandalf) ne se # joignaient plus — 0/8 — alors que TOUTES les paires entre noeuds differents # etaient a 8/8. Douze tentatives faisaient monter le compteur `ctstate INVALID` # de +112 sur asgard et +116 sur gandalf ; `firewall=0` pose, il ne bouge plus. # # Le defaut se deguise en panne reseau : la poignee TCP aboutit (SYN/SYN-ACK # creent l'etat), et ce sont les paquets de DONNEES qui disparaissent. On accuse # le MTU, la frontiere, la zone. Rien de tout cela n'y est pour quelque chose. # # L'INTENTION DECLAREE NE SUFFIT DONC PAS : un tenant declare # `proxmox_clone_parefeu_interface: true` avec `proxmox_clone_pont: vmbr1` comme # valeur PAR DEFAUT, chaque hote la remplacant par son VNet derive. L'hote qui # retombe sur `vmbr1` naitrait arme sur un pont classique. On croise donc # l'intention avec le pont REELLEMENT utilise. Un pont Linux se nomme `vmbrN` ; # un VNet SDN porte le nom derive de sa zone (`t29serv`). firewall: "{{ (proxmox_clone_parefeu_interface | default(false) | bool) and (proxmox_clone_pont is not match('^vmbr[0-9]+$')) }}" # `mtu: 1` est la valeur Proxmox pour « HERITE DU PONT » (virtio uniquement). # # Sans elle, l'invite nait a 1500 quel que soit le pont. En SDN EVPN la zone est a # 1450 (VXLAN coute 50 octets) : la VM emet donc des trames que son propre chemin # ne peut pas encapsuler. Ca ne casse pas franchement — la connexion s'etablit, # les petites requetes passent, les grosses reponses restent suspendues. C'est # exactement la panne que le registre des flux decrit comme « la plus couteuse a # diagnostiquer », et pourquoi il declare l'ICMP « fragmentation necessaire ». # # Mesure du 2026-08-10 : zones `t11` et `t17` a 1450 sur le cluster, les 14 invites # a 1500. Invisible tant que toutes les VM vivent sur le MEME hyperviseur — elles # communiquent alors par le pont local, sans encapsulation. La panne apparaitrait # au premier eclatement de la flotte sur plusieurs noeuds. # # HERITER plutot qu'ecrire 1450 : hors SDN le pont est a 1500 et la VM suit. Une # seule source de verite — celle du pont auquel elle est reellement attachee — et # la valeur reste juste le jour ou la fabric passera aux trames jumbo. # # Le GABARIT le porte aussi (`net0 ... mtu=1`), ce qui couvre les clonages qui ne # passent pas par ce playbook — un clone fait a la main dans l'interface, par # exemple. On garde neanmoins la ligne ici : un gabarit se RECAPTURE (fait le # 2026-08-09), et ce qui n'est pas versionne se perd en silence. Le playbook est # la garantie qui survit a la recapture ; `make mtu-mesurer` verifie le resultat. # # Cette tache s'execute AVANT le demarrage du clone (voir « Demarrer le clone » # plus bas) : aucun risque de rebranchement a chaud. Applique a une VM EN MARCHE, # le meme changement detache et rebranche la carte sans que l'invite reconfigure # son interface — 14 machines coupees d'un coup le 2026-08-10, et il a fallu les # redemarrer. Sur une VM deja en service : poser la config, puis redemarrer. mtu: 1 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" # LE DEMARRAGE DECLARAIT EN PANNE CE QUI FONCTIONNAIT (2026-08-29). # # Quinze VM creees trois a la fois : l'une d'elles a depasse le delai du module # PENDANT QUE PROXMOX GENERAIT ENCORE SON ISO CLOUD-INIT. # # fatal: Reached timeout while waiting for starting VM. # Last line in task before timeout: 'generating cloud-init ISO' # # Elle a demarre juste apres. Le module avait abandonne, pas l'hyperviseur — et # `flotte-creer` a rendu « au moins une VM n'a pas ete creee » alors que les quinze # tournaient. CE FAUX ECHEC ARRETE UNE RECONSTRUCTION : `reconstruire` enchaine # `flotte-creer` puis `deployer-tout`, et se serait arrete la. # # DEUX CORRECTIONS, ET LA SECONDE EST LA VRAIE. Un delai plus genereux (le defaut du # module est court, et la generation d'ISO sur un stockage partage se met en file # quand trois clonages tombent ensemble). Mais un delai reste un pari : on ne fait # donc plus de son expiration un verdict. C'EST L'HYPERVISEUR QUI JUGE, en repondant # ce qu'il fait tourner — meme principe que P52, ou l'agent invite juge la # materialisation a la place d'une reponse SSH. - 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 timeout: "{{ proxmox_clone_delai_demarrage | default(180) | int }}" register: proxmox_clone_demarrage failed_when: false when: proxmox_clone_demarrer | default(true) | bool # L'HYPERVISEUR EST LE JUGE. On lui demande ce qu'il fait tourner, on ne deduit rien # du sort de la tache precedente. Les tentatives couvrent le cas ou il finissait # encore son travail au moment ou le module a renonce. - name: Confirmer aupres de l'hyperviseur que la VM tourne community.general.proxmox_vm_info: 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 }}" type: qemu register: proxmox_clone_etat until: (proxmox_clone_etat.proxmox_vms | default([]) | map(attribute='status') | first | default('')) == 'running' retries: 12 delay: 10 changed_when: false when: proxmox_clone_demarrer | default(true) | bool - name: Refuser si la VM ne tourne toujours pas ansible.builtin.assert: that: - (proxmox_clone_etat.proxmox_vms | default([]) | map(attribute='status') | first | default('')) == 'running' fail_msg: >- {{ proxmox_clone_nom }} (VMID {{ proxmox_clone_vmid }}) ne tourne pas apres deux minutes d'attente. Ce n'est plus un delai trop court : l'hyperviseur lui-meme la rapporte « {{ proxmox_clone_etat.proxmox_vms | default([]) | map(attribute='status') | first | default('introuvable') }} ». success_msg: >- {{ proxmox_clone_nom }} tourne {{ '(le module avait renonce avant l hyperviseur)' if proxmox_clone_demarrage.failed | default(false) else '' }} 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 }}"