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>
141 lines
6.2 KiB
YAML
141 lines
6.2 KiB
YAML
---
|
|
# DESTRUCTIF : supprime des VM Proxmox par VMID. Réservé à un from-zero explicite.
|
|
# Sécurités : n'agit QUE sur les VMID passés en `cibles_vmid` ET présents dans le
|
|
# cluster ; refuse si le modèle (proxmox_clone_vmid_modele) est dans la liste.
|
|
- name: Supprimer des VM Debian du cluster Proxmox (from-zero)
|
|
hosts: localhost
|
|
connection: local
|
|
gather_facts: false
|
|
become: false
|
|
|
|
vars:
|
|
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"
|
|
# cibles_vmid : liste d'entiers, passée en -e (sinon dérivée des hôtes actifs).
|
|
cibles_vmid: "{{ groups['hotes_actifs'] | map('extract', hostvars, 'proxmox_vmid') | map('int') | list }}"
|
|
|
|
tasks:
|
|
- name: Localiser les fichiers de variables Proxmox
|
|
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
|
|
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)
|
|
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
|
|
|
|
- name: Résoudre les identifiants d'API
|
|
ansible.builtin.set_fact:
|
|
api_host: "{{ proxmox_api_host | default(lookup('env', 'PROXMOX_API_HOST'), true) }}"
|
|
api_port: "{{ proxmox_api_port | default('8006', true) }}"
|
|
api_user: "{{ proxmox_api_user | default(lookup('env', 'PROXMOX_API_USER'), true) }}"
|
|
# `split('!')[-1]` : proxmoxer recompose `utilisateur!nom` a partir d'api_user
|
|
# et d'api_token_id. La forme COMPLETE donne un 401 muet (verifie contre le
|
|
# cluster le 2026-08-03). Sans effet si la voute stocke deja le nom seul.
|
|
api_tid: "{{ (proxmox_api_token_id | default(lookup('env', 'PROXMOX_API_TOKEN_ID'), true)) | string | split('!') | last }}"
|
|
api_secret: "{{ proxmox_api_token_secret | default(lookup('env', 'PROXMOX_API_TOKEN_SECRET'), true) }}"
|
|
no_log: true
|
|
|
|
- name: Interroger l'inventaire du cluster
|
|
ansible.builtin.uri:
|
|
url: "https://{{ api_host }}:{{ api_port }}/api2/json/cluster/resources?type=vm"
|
|
method: GET
|
|
headers:
|
|
Authorization: "PVEAPIToken={{ api_user }}!{{ api_tid }}={{ api_secret }}"
|
|
validate_certs: "{{ proxmox_validate_certs | default(false) | bool }}"
|
|
register: cluster_vms
|
|
check_mode: false
|
|
no_log: true
|
|
|
|
- name: Établir la carte VMID -> nœud pour nos cibles présentes
|
|
ansible.builtin.set_fact:
|
|
vm_a_supprimer: >-
|
|
{{ cluster_vms.json.data
|
|
| selectattr('vmid', 'in', cibles_vmid | map('int') | list)
|
|
| list }}
|
|
|
|
- name: "SÉCURITÉ — le modèle n'est PAS ciblé (VMID {{ proxmox_clone_vmid_modele }})"
|
|
ansible.builtin.assert:
|
|
that: "(proxmox_clone_vmid_modele | int) not in (cibles_vmid | map('int') | list)"
|
|
fail_msg: "REFUS : le VMID du modèle est dans la liste à supprimer."
|
|
|
|
- name: Récapitulatif avant suppression
|
|
ansible.builtin.debug:
|
|
msg: >-
|
|
Suppression de {{ vm_a_supprimer | length }} VM :
|
|
{{ vm_a_supprimer | map(attribute='name') | list }}
|
|
(VMID {{ vm_a_supprimer | map(attribute='vmid') | list }})
|
|
|
|
- name: Arrêter chaque VM (force)
|
|
community.general.proxmox_kvm:
|
|
api_host: "{{ api_host }}"
|
|
api_port: "{{ api_port | int }}"
|
|
api_user: "{{ api_user }}"
|
|
api_token_id: "{{ api_tid }}"
|
|
api_token_secret: "{{ api_secret }}"
|
|
validate_certs: "{{ proxmox_validate_certs | default(false) | bool }}"
|
|
node: "{{ item.node }}"
|
|
vmid: "{{ item.vmid | int }}"
|
|
state: stopped
|
|
force: true
|
|
timeout: 120
|
|
loop: "{{ vm_a_supprimer }}"
|
|
loop_control:
|
|
label: "{{ item.name }} ({{ item.vmid }})"
|
|
no_log: true
|
|
failed_when: false
|
|
|
|
- name: Supprimer chaque VM
|
|
community.general.proxmox_kvm:
|
|
api_host: "{{ api_host }}"
|
|
api_port: "{{ api_port | int }}"
|
|
api_user: "{{ api_user }}"
|
|
api_token_id: "{{ api_tid }}"
|
|
api_token_secret: "{{ api_secret }}"
|
|
validate_certs: "{{ proxmox_validate_certs | default(false) | bool }}"
|
|
node: "{{ item.node }}"
|
|
vmid: "{{ item.vmid | int }}"
|
|
state: absent
|
|
timeout: 120
|
|
loop: "{{ vm_a_supprimer }}"
|
|
loop_control:
|
|
label: "{{ item.name }} ({{ item.vmid }})"
|
|
no_log: true
|