Set-OPS-Public/scripts/site_raser.py

166 lines
6.9 KiB
Python
Raw Normal View History

le site prend l index 37, et la capacite de le raser existe enfin SITE-Chezlepro passe de 10.0.31-36 (tapes a la main, derives de rien) et 10.17.0.0/24 (emprunte au supernet du locataire) a son PROPRE index : gestion en 10.37.0.0/24, zones en 10.37.31-36. OPS-Chezlepro garde 17. CE QUE L OPERATION A REVELE. Il n existait aucun moyen de raser le site : raser.py ne vise que l instance active, un locataire. Ce n est donc pas que personne n avait essaye de le reconstruire depuis zero — l outil n en offrait pas le moyen, et la limite se lisait partout sans que sa cause soit nommee. make site-raser reprend les quatre verrous de raser.py. TROIS FOIS LE MEME DEFAUT, attrape par l exploitant. La declaration decrit la CIBLE pendant que l outillage s en sert pour joindre l EXISTANT : renumeroter le rebond avant de bouger la patte a rendu le site injoignable. Regle posee : le renumerotage declaratif vient APRES la derniere operation qui a besoin de l ancienne infrastructure. nftables_admin_ssh etait une liste a la main qui devait suivre le plan d administration. Elle a pris du retard le jour meme. Le plan de frontiere n a propose AUCUNE creation ni suppression de regle — seulement douze contenus d alias. Les regles visent des alias par leur nom : un renumerotage complet se reduit a changer ce que les noms designent. Routes des trois hyperviseurs refaites, declarees ET vives, avec sauvegarde de /etc/network/interfaces. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-12 16:32:06 -04:00
#!/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())