Set-OPS-Public/playbooks/proxmox/cloner_vm_debian.yml
Daniel Allaire ac48b7650b
Some checks are pending
verifier / verifier (push) Waiting to run
proxmox : le pare-feu ne s'arme que dans le SDN — et 45e preuve
Le decoupage du site en quatre zones a revele un defaut qui dormait dans le
moteur. `firewall=1` sur l'interface d'une VM 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, et le flux ne quitte jamais
l'hyperviseur. Sur un pont classique route par une frontiere externe, 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, 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.

La mesure a tranche :

  ops(asgard)  -> pki(asgard)     0/8    dns(gandalf) -> cache(gandalf)  0/8
  ops(asgard)  -> forge(vishnu)   6/8    dns(gandalf) -> pki(asgard)     6/8
  ops(asgard)  -> cache(gandalf)  8/8    dns(gandalf) -> forge(vishnu)   8/8

Toutes les paires intra-noeud echouent, toutes les paires inter-noeuds passent.
Douze tentatives faisaient monter le compteur `ctstate INVALID` de +112 sur
asgard et +116 sur gandalf ; `firewall=0` pose, il ne bouge plus — 0 sur les
deux, pour le meme trafic.

Le defaut se deguise en panne reseau : la poignee TCP ABOUTIT, et ce sont les
paquets de DONNEES qui disparaissent. L'AC le disait dans son propre journal
(`TLS handshake error ... i/o timeout` : elle accepte et attend un ClientHello
qui n'arrive jamais). Ecartes un par un, par la mesure : regles identiques champ
par champ, alias corrects, assignation des interfaces confirmee, ARP et routes
saines, IPS desactive, shaper vide, NAT source limite a `wan`, MTU a 1500 de
bout en bout — et la taille sans effet, 100 octets se perdant comme 1460.

L'INTENTION DECLAREE NE SUFFIT 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. `cloner_vm_debian.yml`
croise donc l'intention avec le pont REELLEMENT utilise, et le dit quand il
desarme — un desarmement muet serait le meme piege, en silence.

P45 EVALUE l'expression du playbook sur quatre cas plutot que d'en lire le
texte, dont un qui DOIT rendre vrai : sans lui, une expression constamment
fausse passerait la preuve sans rien garantir. Controle negatif verifie — le
drapeau remis a plat fait echouer la preuve.

Ce commit emporte aussi le desarmement d'`unattended-upgrades` dans
`common_packages`, jusqu'ici applique sur les machines mais pas versionne : le
verrou dpkg tenu 48 minutes par une vague de maj automatiques a coute deux
deploiements.

Le site a revele ce defaut parce qu'il a ete le premier a porter plusieurs
zones. Ce n'est pas une particularite du site : un tenant derive les siennes du
meme principe.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 19:50:53 -04:00

596 lines
33 KiB
YAML

---
- 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
- 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:<noeud>:<pid>:...`. 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
- 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 : etat « {{ proxmox_clone_tache.json.data.status | default('?')
}} », sortie « {{ 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.
- >-
(proxmox_clone_tache.json.data.status | default('')) != 'stopped'
or (proxmox_clone_tache.json.data.exitstatus | default('')) != 'OK'
- 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 }}"
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
# 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"
- 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 }}"