Some checks are pending
verifier / verifier (push) Waiting to run
Demande de l'exploitant : « l'underlay et son tenant doivent avoir chacun sa voute ». CE QUI ETAIT FAUX. Le jeton d'API du cluster et la cle d'API de la frontiere vivaient dans la voute de CHAQUE tenant. Patient 0 a du les recopier pour exister. Consequence : on ne pouvait plus revoquer l'acces d'un locataire sans le revoquer pour tous — la faute des neuf copies, appliquee aux secrets. CE QUI EST POSE : - `underlay.vault.yml`, chez l'hebergeur, a cote d'underlay.yml. Quatre secrets deplaces (jeton Proxmox, cle et secret d'API OPNsense), retires des deux voutes de tenants. - `proxmox_api.voute()` lit l'underlay APRES le tenant, donc l'hebergeur fait foi ; un site non encore migre continue de fonctionner sur son ancienne voute. - `appliquer_opnsense._voute()` n'a plus sa propre lecture : elle appelle celle du cluster. La reecrire aurait fait une dixieme copie le jour ou l'on refermait les neuf autres. - Le playbook de clonage charge la voute de l'underlay APRES celle du tenant, par la meme derivation que proxmox-hebergeur.yml : le symlink designe deja l'hebergeur. - `voute.py` sait que ces secrets ne sont plus attendus chez un tenant (P18). EPROUVE SUR LE REEL, apres retrait des cles chez les deux tenants : l'API du cluster repond, le devis de placement est conforme pour les deux ecosystemes, le devis de frontiere se genere (86 objets), le SDN est convergent. UNE PRECAUTION APPRISE EN CHEMIN : le premier essai a ecrit la voute EN CLAIR avant de la chiffrer, et le chiffrement a echoue — il a fallu detruire le fichier. La sequence est desormais l'inverse : chiffrer dans un dossier de travail, ne deposer que le resultat. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
487 lines
26 KiB
YAML
487 lines
26 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 }}
|
|
|
|
- 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_clone_noeud }}/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) }}"
|
|
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
|
|
|
|
# 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:
|
|
url: >-
|
|
{{ proxmox_api_racine }}/api2/json/nodes/{{ proxmox_clone_noeud
|
|
}}/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_noeud }}.
|
|
when:
|
|
- not proxmox_clone_deja_la
|
|
- proxmox_clone_tache.json.data.exitstatus | default('OK') != '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
|
|
|
|
- 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 }}"
|
|
# `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 }}"
|