#!/usr/bin/env python3 """DESTRUCTIF : detruit les VM du SITE derivees de son underlay. POURQUOI CETTE COMMANDE N'EXISTAIT PAS, ET CE QUE CA A COUTE (2026-09-12). `raser.py` ne vise que l'instance ACTIVE — un locataire. Rien, nulle part, ne detruisait les machines du site. Ce n'est donc pas que personne n'avait essaye de reconstruire le site depuis zero : **l'outil n'en offrait pas le moyen**. Cette absence se lisait partout ailleurs comme une limite honnete — « l'infrastructure d'accueil n'a jamais ete reconstruite depuis zero » — sans que sa CAUSE soit nommee. Une limite dont on ignore la cause finit par ressembler a une fatalite. Et elle rendait la reconstruction du site non reproductible : detruire sept VM a la main dans l'interface de Proxmox n'est pas une preuve, c'est un souvenir. LES QUATRE VERROUS DE `raser.py`, REPRIS TELS QUELS : 1. la liste vient de l'UNDERLAY, jamais d'un argument libre ; 2. un VMID qui porte un AUTRE nom sur le cluster fait REFUSER l'ensemble — ce sont des machines qui ne nous appartiennent pas ; 3. `--confirmer` est exige ; 4. il faut NOMMER le site, et que ce nom corresponde a celui qui est monte. Usage : python3 scripts/site_raser.py # affiche, ne detruit rien python3 scripts/site_raser.py --site SITE-Chezlepro --confirmer """ from __future__ import annotations import argparse import subprocess import sys import time from pathlib import Path RACINE = Path(__file__).resolve().parent.parent sys.path.insert(0, str(RACINE / "scripts")) from proxmox_api import Cluster # noqa: E402 from raser import _attendre_tache # noqa: E402 import underlay as underlay_mod # noqa: E402 def _machines_du_site() -> list[tuple[str, int, str]]: """[(nom, vmid, noeud)] tels que l'UNDERLAY les derive — la meme source que `site-creer`. Interroger une autre source reviendrait a fabriquer une seconde liste, et c'est exactement ce que ce depot passe son temps a supprimer.""" noms = subprocess.run([sys.executable, str(RACINE / "scripts" / "site_machines.py"), "lister"], capture_output=True, text=True).stdout.split() couples = [] for nom in noms: p = subprocess.run([sys.executable, str(RACINE / "scripts" / "site_machines.py"), "parametres-proxmox", "--machine", nom], capture_output=True, text=True).stdout vals = dict(l.split("=", 1) for l in p.splitlines() if "=" in l) vmid = vals.get("SETOPS_VMID", "").strip() noeud = vals.get("SETOPS_NOEUD", "").strip() if vmid.isdigit(): couples.append((nom, int(vmid), noeud)) return couples def main() -> int: ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) ap.add_argument("--site", help="nom du depot de site a raser (doit correspondre au monte)") ap.add_argument("--machine", help="ne raser qu'une machine (les quatre verrous tiennent)") ap.add_argument("--confirmer", action="store_true") a = ap.parse_args() chemin = underlay_mod.chemin() if not chemin: print("Aucun underlay monte : aucun site a raser.") return 2 nom_monte = Path(chemin).resolve().parent.name couples = _machines_du_site() if a.machine: couples = [c for c in couples if c[0] == a.machine] if not couples: print(f"La machine « {a.machine} » n'est pas a l'underlay : rien a raser.") return 2 if not couples: print("L'underlay ne derive aucune machine de site : rien a raser.") return 0 cl, _ = Cluster.depuis_hebergeur() rep = cl("/cluster/resources?type=vm") if err := cl.rate(rep): raise SystemExit(f"Cluster injoignable : {err}") reelles = {int(v["vmid"]): v for v in rep if v.get("type") == "qemu"} presentes, absentes, usurpees = [], [], [] for nom, vmid, _noeud in sorted(couples, key=lambda c: c[1]): v = reelles.get(vmid) if v is None: absentes.append((nom, vmid)) elif v.get("name") != nom: usurpees.append((nom, vmid, v.get("name"))) else: presentes.append((nom, vmid, v.get("node"))) print(f"Site monte : « {nom_monte} » ({chemin})\n") for nom, vmid, noeud in presentes: print(f" À DÉTRUIRE {vmid} {nom:16} sur {noeud}") for nom, vmid in absentes: print(f" absente {vmid} {nom:16} (rien à faire)") for nom, vmid, autre in usurpees: print(f" CONFLIT {vmid} l'underlay dit « {nom} », le cluster dit « {autre} »") if usurpees: print(f"\nREFUS : {len(usurpees)} VMID de l'underlay portent un AUTRE nom sur le " f"cluster.\nCe sont des machines qui ne nous appartiennent pas. Les renommer " f"ou corriger\nl'underlay avant de raser quoi que ce soit — aucune VM n'a " f"ete touchee.") return 2 if not presentes: print("\nAucune machine de l'underlay n'est presente : rien a faire.") return 0 if not a.confirmer: print(f"\n{len(presentes)} VM du SITE seraient DETRUITES, disques compris.\n" f"Le site porte le depot de sauvegarde de CHAQUE locataire et la racine de\n" f"son AC : sortir le depot hors site AVANT (`make depot-hors-site VERS=…`).\n" f"Relancer avec : make site-raser CONFIRMER=true SITE={nom_monte}" + (f" MACHINE={a.machine}" if a.machine else "")) return 0 if (a.site or "") != nom_monte: print(f"\nREFUS : il faut NOMMER le site a raser.\n" f"Monte : « {nom_monte} » ; recu : « {a.site or '(rien)'} ».\n" f"Taper le nom est ce qui distingue l'essai du batiment reel.") return 2 echecs = 0 for nom, vmid, noeud in presentes: print(f"\n {vmid} {nom} — arret…", flush=True) cl(f"/nodes/{noeud}/qemu/{vmid}/status/stop", "POST") for _ in range(30): time.sleep(2) st = cl(f"/nodes/{noeud}/qemu/{vmid}/status/current") if isinstance(st, dict) and st.get("status") == "stopped": break r = cl(f"/nodes/{noeud}/qemu/{vmid}?purge=1&destroy-unreferenced-disks=1", "DELETE") if err := cl.rate(r): print(f" {vmid} {nom} — ECHEC : {err}") echecs += 1 continue # ATTENDRE LE RESULTAT, PAS L'ACCUSE DE RECEPTION — meme lecon que `raser.py` : # l'API rend un UPID et la main immediatement, et la tache peut echouer APRES. etat = _attendre_tache(cl, noeud, r) if etat == "OK": print(f" {vmid} {nom} — detruite") else: print(f" {vmid} {nom} — ECHEC : {etat}") echecs += 1 print(f"\n{len(presentes) - echecs}/{len(presentes)} VM du site detruites.") return 1 if echecs else 0 if __name__ == "__main__": raise SystemExit(main())