Reconnaissance en lecture seule de l'API du cluster, avec le jeton de la voûte.
Trois valeurs devinées étaient fausses, et deux défauts bloquants sont apparus.
Corrigé d'après le cluster
- stockages : truenas-dbsql manquait ; le catalogue ne garde que ceux qui
portent `images` (PBS, cephFS, local et truenas iSCSI n'accueillent pas de
disque de VM) ;
- ponts : vmbr0 avait été omis, et l'uniformité sur les trois nœuds n'avait pas
été vérifiée — un pont partiel empêche la VM de démarrer sur certains nœuds.
Pools
Chezlepro-17 et Technolibre-11 créés, dérivés comme le reste. Les pools
Env.Tenant antérieurs (Prod.Chezlepro, Lab.KBR...) sont l'ancien monde : on n'y
touche pas et on n'y verse pas la flotte générée. Diff réel : 2 pools ajoutés,
0 retiré, 1 VM sur 38 déplacée — infra-pki-01, qui n'avait aucun pool.
Défaut bloquant : make creer-vm aurait échoué en 401
proxmoxer recompose `utilisateur!nom` à partir d'api_user et d'api_token_id. La
voûte stocke la forme complète, que les playbooks passaient telle quelle, d'où
un 401 muet — alors que le même jeton fonctionne en curl. Mesuré des deux côtés
avec un module en lecture seule : forme complète = 401, forme courte = OK.
Normalisation par split('!') | last, qui accepte les deux écritures.
Reliquat proxmox.vault.yml supprimé
Toléré « en compatibilité », il restait le seul porteur du jeton chez
Technolibre — et comme *.vault.yml est gitignoré, ce jeton ne voyageait avec
aucun dépôt : une voûte unique (D-19) qui ne l'était pas. Migration faite en
mémoire, avec relecture et aller-retour de chiffrement vérifiés avant écriture ;
fichier supprimé, listes de chargement des playbooks nettoyées, validé par un
appel API réel ne chargeant que all/vault.yml.
La garde qui manquait
voute.py verifier ne comparait que le gabarit — il disait « complet » pendant
qu'un secret vivait ailleurs. Il contrôle maintenant aussi la voûte réelle quand
ANSIBLE_VAULT_PASSWORD_FILE la rend déchiffrable : noms de clés seulement,
jamais de valeur, et vérification sautée sans mot de passe.
Elle a trouvé un second trou dès son premier passage : la voûte réelle de
Technolibre n'a ni vault_nextcloud_admin ni vault_nextcloud_oidc, que le plan
exige. Non corrigé — générer ces secrets est une décision, et celui d'OIDC doit
correspondre à ce que Keycloak connaîtra.
27 preuves OK. --syntax-check et ansible-lint (production) sur les playbooks.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
264 lines
14 KiB
YAML
264 lines
14 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"
|
|
|
|
tasks:
|
|
- name: Verifier les fichiers de variables Proxmox optionnels
|
|
ansible.builtin.stat:
|
|
path: "{{ playbook_dir }}/../../instance/inventories/{{ item.0 }}/group_vars/{{ item.1 }}"
|
|
loop: "{{ setops_inventaires | product(setops_gv_proxmox) | list }}"
|
|
loop_control:
|
|
label: "{{ item.0 }}/{{ item.1 }}"
|
|
register: proxmox_fichiers_variables
|
|
|
|
- name: Charger les variables Proxmox non sensibles disponibles
|
|
ansible.builtin.include_vars:
|
|
file: "{{ item.stat.path }}"
|
|
loop: "{{ proxmox_fichiers_variables.results }}"
|
|
when:
|
|
- item.stat.exists
|
|
- item.stat.path is not search('vault')
|
|
|
|
- name: Localiser le fichier Proxmox de l'hebergeur
|
|
ansible.builtin.stat:
|
|
path: "{{ setops_proxmox_hebergeur }}"
|
|
register: proxmox_fichier_hebergeur
|
|
|
|
- name: Charger le cluster de l'hebergeur (autoritaire sur ses propres cles)
|
|
ansible.builtin.include_vars:
|
|
file: "{{ setops_proxmox_hebergeur }}"
|
|
when: proxmox_fichier_hebergeur.stat.exists
|
|
|
|
- name: Charger les secrets Proxmox Vault
|
|
block:
|
|
- name: Charger les secrets de la voute Proxmox
|
|
ansible.builtin.include_vars:
|
|
file: "{{ item.stat.path }}"
|
|
loop: "{{ proxmox_fichiers_variables.results }}"
|
|
when:
|
|
- item.stat.exists
|
|
- item.stat.path is search('vault')
|
|
no_log: true
|
|
rescue:
|
|
- name: Expliquer l echec de chargement du Vault
|
|
ansible.builtin.fail:
|
|
msg: >-
|
|
Impossible de charger la voute Proxmox (group_vars/all/vault.yml ou
|
|
proxmox.vault.yml). Si elle est chiffree avec Ansible Vault, fournir son
|
|
mot de passe ou definir ANSIBLE_VAULT_PASSWORD_FILE avant de relancer make creer-vm.
|
|
|
|
- name: Appliquer les valeurs Proxmox avec repli environnement
|
|
ansible.builtin.set_fact:
|
|
proxmox_api_host_effectif: "{{ proxmox_api_host | default(lookup('env', 'PROXMOX_API_HOST'), true) }}"
|
|
proxmox_api_user_effectif: "{{ proxmox_api_user | default(lookup('env', 'PROXMOX_API_USER'), true) }}"
|
|
# `split('!')[-1]` : proxmoxer recompose lui-meme `utilisateur!nom` a partir de
|
|
# `api_user` et `api_token_id`. Lui passer la forme COMPLETE produit
|
|
# `ansible@pve!ansible@pve!nom` et un 401 muet — le meme jeton fonctionne en
|
|
# HTTP direct, ce qui rend le diagnostic trompeur. Verifie contre le cluster le
|
|
# 2026-08-03 : forme complete = 401, forme courte = OK. Sans effet si la voute
|
|
# stocke deja le nom seul, donc les deux ecritures restent acceptees.
|
|
proxmox_api_token_id_effectif: "{{ (proxmox_api_token_id | default(lookup('env', 'PROXMOX_API_TOKEN_ID'), true)) | string | split('!') | last }}"
|
|
proxmox_api_token_secret_effectif: "{{ proxmox_api_token_secret | default(lookup('env', 'PROXMOX_API_TOKEN_SECRET'), true) }}"
|
|
proxmox_api_port_effectif: "{{ proxmox_api_port | default(lookup('env', 'PROXMOX_API_PORT'), true) }}"
|
|
no_log: true
|
|
|
|
- name: Valider les parametres obligatoires Proxmox
|
|
ansible.builtin.assert:
|
|
that:
|
|
- proxmox_api_host_effectif | length > 0
|
|
- proxmox_api_user_effectif | length > 0
|
|
- proxmox_api_token_id_effectif | length > 0
|
|
- proxmox_api_token_secret_effectif | length > 0
|
|
- proxmox_clone_nom is defined
|
|
- proxmox_clone_nom | length > 0
|
|
- proxmox_clone_vmid_modele is defined
|
|
- proxmox_clone_vmid is defined
|
|
- proxmox_clone_noeud is defined
|
|
- proxmox_clone_noeud | length > 0
|
|
- proxmox_clone_ipconfig0 is defined
|
|
- proxmox_clone_ipconfig0 | length > 0
|
|
fail_msg: "Parametres Proxmox incomplets. Verifier proxmox.yml, la voute (all/vault.yml ou proxmox.vault.yml) et les variables make propres a la VM."
|
|
|
|
- name: Lire la cle publique SSH depuis le fichier fourni
|
|
ansible.builtin.set_fact:
|
|
proxmox_clone_sshkeys: "{{ lookup('ansible.builtin.file', proxmox_clone_cle_publique_fichier) }}"
|
|
when:
|
|
- proxmox_clone_cle_publique is not defined
|
|
- proxmox_clone_cle_publique_fichier is defined
|
|
- proxmox_clone_cle_publique_fichier | length > 0
|
|
|
|
- name: Utiliser la cle publique SSH fournie directement
|
|
ansible.builtin.set_fact:
|
|
proxmox_clone_sshkeys: "{{ proxmox_clone_cle_publique }}"
|
|
when:
|
|
- proxmox_clone_cle_publique is defined
|
|
- proxmox_clone_cle_publique | length > 0
|
|
|
|
- name: Construire les serveurs DNS Cloud-Init
|
|
ansible.builtin.set_fact:
|
|
proxmox_clone_nameservers: >-
|
|
{{
|
|
(proxmox_clone_dns | default('') | split(','))
|
|
| map('trim')
|
|
| reject('equalto', '')
|
|
| list
|
|
}}
|
|
|
|
- name: Verifier la bibliotheque Python proxmoxer locale
|
|
ansible.builtin.command:
|
|
cmd: "{{ ansible_playbook_python }} -c 'import proxmoxer'"
|
|
changed_when: false
|
|
failed_when: false
|
|
register: proxmoxer_verification
|
|
|
|
- name: Refuser si proxmoxer est absent
|
|
ansible.builtin.fail:
|
|
msg: >-
|
|
La bibliotheque Python proxmoxer est absente pour {{ ansible_playbook_python }}.
|
|
Installer le paquet systeme approprie, par exemple
|
|
sudo apt install python3-proxmoxer, ou installer proxmoxer dans
|
|
l'environnement Python utilise par Ansible, puis relancer make creer-vm.
|
|
when: proxmoxer_verification.rc != 0
|
|
|
|
- name: S'assurer que le pool du tenant existe
|
|
# Un pool par tenant : la console Proxmox affiche le NOM, et onze serveurs
|
|
# portent le meme d'un tenant a l'autre. Le pool restitue l'appartenance sans
|
|
# rien renommer. Il doit exister AVANT le clone — `proxmox_kvm` echoue sur un
|
|
# pool inconnu, et l'API ne sait pas changer le pool d'une VM deja creee.
|
|
community.general.proxmox_pool:
|
|
api_host: "{{ proxmox_api_host_effectif }}"
|
|
api_port: "{{ proxmox_api_port_effectif | int if proxmox_api_port_effectif | length > 0 else omit }}"
|
|
api_user: "{{ proxmox_api_user_effectif }}"
|
|
api_token_id: "{{ proxmox_api_token_id_effectif }}"
|
|
api_token_secret: "{{ proxmox_api_token_secret_effectif }}"
|
|
validate_certs: "{{ proxmox_validate_certs | default(false) | bool }}"
|
|
poolid: "{{ proxmox_clone_pool }}"
|
|
comment: "Tenant Set-OPS — genere, ne pas renommer a la main"
|
|
state: present
|
|
when: proxmox_clone_pool | default('', true) | length > 0
|
|
|
|
- name: Cloner la VM depuis le modele
|
|
community.general.proxmox_kvm:
|
|
api_host: "{{ proxmox_api_host_effectif }}"
|
|
api_port: "{{ proxmox_api_port_effectif | int if proxmox_api_port_effectif | length > 0 else omit }}"
|
|
api_user: "{{ proxmox_api_user_effectif }}"
|
|
api_token_id: "{{ proxmox_api_token_id_effectif }}"
|
|
api_token_secret: "{{ proxmox_api_token_secret_effectif }}"
|
|
validate_certs: "{{ proxmox_validate_certs | default(false) | bool }}"
|
|
node: "{{ proxmox_clone_noeud }}"
|
|
clone: "{{ proxmox_clone_source_nom | default('modele-debian13') }}"
|
|
vmid: "{{ proxmox_clone_vmid_modele | int }}"
|
|
newid: "{{ proxmox_clone_vmid | int }}"
|
|
name: "{{ proxmox_clone_nom }}"
|
|
cores: "{{ proxmox_clone_coeurs | int if proxmox_clone_coeurs is defined and proxmox_clone_coeurs | string | length > 0 else omit }}"
|
|
memory: "{{ proxmox_clone_memoire | int if proxmox_clone_memoire is defined and proxmox_clone_memoire | string | length > 0 else omit }}"
|
|
storage: "{{ proxmox_clone_stockage | default(omit, true) }}"
|
|
format: "{{ proxmox_clone_format | default(omit, true) }}"
|
|
full: "{{ proxmox_clone_complet | default(true) | bool }}"
|
|
# A la CREATION seulement : l'API ne permet pas de le changer ensuite.
|
|
pool: "{{ proxmox_clone_pool | default(omit, true) }}"
|
|
timeout: "{{ proxmox_clone_timeout | default(600) | int }}"
|
|
|
|
- name: Ajuster le reseau de la VM clonee
|
|
community.general.proxmox_nic:
|
|
api_host: "{{ proxmox_api_host_effectif }}"
|
|
api_port: "{{ proxmox_api_port_effectif | int if proxmox_api_port_effectif | length > 0 else omit }}"
|
|
api_user: "{{ proxmox_api_user_effectif }}"
|
|
api_token_id: "{{ proxmox_api_token_id_effectif }}"
|
|
api_token_secret: "{{ proxmox_api_token_secret_effectif }}"
|
|
validate_certs: "{{ proxmox_validate_certs | default(false) | bool }}"
|
|
vmid: "{{ proxmox_clone_vmid | int }}"
|
|
interface: "{{ proxmox_clone_interface | default('net0') }}"
|
|
model: virtio
|
|
bridge: "{{ proxmox_clone_pont }}"
|
|
tag: "{{ proxmox_clone_vlan | int if proxmox_clone_vlan is defined and proxmox_clone_vlan | string | length > 0 else omit }}"
|
|
firewall: "{{ proxmox_clone_parefeu_interface | default(false) | bool }}"
|
|
state: present
|
|
when:
|
|
- proxmox_clone_pont is defined
|
|
- proxmox_clone_pont | length > 0
|
|
|
|
- name: Agrandir le disque principal du clone (grow-only)
|
|
community.general.proxmox_disk:
|
|
api_host: "{{ proxmox_api_host_effectif }}"
|
|
api_port: "{{ proxmox_api_port_effectif | int if proxmox_api_port_effectif | length > 0 else omit }}"
|
|
api_user: "{{ proxmox_api_user_effectif }}"
|
|
api_token_id: "{{ proxmox_api_token_id_effectif }}"
|
|
api_token_secret: "{{ proxmox_api_token_secret_effectif }}"
|
|
validate_certs: "{{ proxmox_validate_certs | default(false) | bool }}"
|
|
vmid: "{{ proxmox_clone_vmid | int }}"
|
|
disk: "{{ proxmox_clone_disque | default('scsi0') }}"
|
|
size: "{{ proxmox_clone_taille_disque }}"
|
|
state: resized
|
|
register: proxmox_redim_disque
|
|
# Le disque derive du plan est un MINIMUM. Si le template est deja plus grand,
|
|
# Proxmox refuse de retrecir : on tolere ce cas (grow-only), la VM garde le
|
|
# disque du template. Toute autre erreur reste bloquante.
|
|
failed_when:
|
|
- proxmox_redim_disque is failed
|
|
- "'shrinking disks is not supported' not in (proxmox_redim_disque.msg | default(''))"
|
|
when:
|
|
- proxmox_clone_taille_disque is defined
|
|
- proxmox_clone_taille_disque | length > 0
|
|
|
|
- name: Configurer Cloud-Init sur le clone
|
|
community.general.proxmox_kvm:
|
|
api_host: "{{ proxmox_api_host_effectif }}"
|
|
api_port: "{{ proxmox_api_port_effectif | int if proxmox_api_port_effectif | length > 0 else omit }}"
|
|
api_user: "{{ proxmox_api_user_effectif }}"
|
|
api_token_id: "{{ proxmox_api_token_id_effectif }}"
|
|
api_token_secret: "{{ proxmox_api_token_secret_effectif }}"
|
|
validate_certs: "{{ proxmox_validate_certs | default(false) | bool }}"
|
|
node: "{{ proxmox_clone_noeud }}"
|
|
vmid: "{{ proxmox_clone_vmid | int }}"
|
|
name: "{{ proxmox_clone_nom }}"
|
|
update: true
|
|
ciuser: "{{ proxmox_clone_ciuser | default(omit, true) }}"
|
|
sshkeys: "{{ proxmox_clone_sshkeys | default(omit) }}"
|
|
ipconfig:
|
|
ipconfig0: "{{ proxmox_clone_ipconfig0 }}"
|
|
nameservers: "{{ proxmox_clone_nameservers if proxmox_clone_nameservers | length > 0 else omit }}"
|
|
searchdomains: "{{ proxmox_clone_domaines_recherche | default(omit, true) }}"
|
|
agent: "enabled=1"
|
|
|
|
- name: Demarrer le clone
|
|
community.general.proxmox_kvm:
|
|
api_host: "{{ proxmox_api_host_effectif }}"
|
|
api_port: "{{ proxmox_api_port_effectif | int if proxmox_api_port_effectif | length > 0 else omit }}"
|
|
api_user: "{{ proxmox_api_user_effectif }}"
|
|
api_token_id: "{{ proxmox_api_token_id_effectif }}"
|
|
api_token_secret: "{{ proxmox_api_token_secret_effectif }}"
|
|
validate_certs: "{{ proxmox_validate_certs | default(false) | bool }}"
|
|
node: "{{ proxmox_clone_noeud }}"
|
|
vmid: "{{ proxmox_clone_vmid | int }}"
|
|
name: "{{ proxmox_clone_nom }}"
|
|
state: started
|
|
when: proxmox_clone_demarrer | default(true) | bool
|
|
|
|
- name: Afficher le resume du clone
|
|
ansible.builtin.debug:
|
|
msg:
|
|
- "VM creee: {{ proxmox_clone_nom }} (VMID {{ proxmox_clone_vmid }})"
|
|
- "Pool: {{ proxmox_clone_pool | default('(aucun)', true) }}"
|
|
- "Cloud-Init ipconfig0: {{ proxmox_clone_ipconfig0 }}"
|