From 0c55aafc40d5906e6c26639767a37d65763fe755 Mon Sep 17 00:00:00 2001 From: Daniel Allaire Date: Sat, 12 Sep 2026 16:32:06 -0400 Subject: [PATCH] le site prend l index 37, et la capacite de le raser existe enfin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q --- Makefile | 13 +++ docs/audit/preuve-2026-09-12.md | 8 +- scripts/site_raser.py | 165 ++++++++++++++++++++++++++++++++ 3 files changed, 182 insertions(+), 4 deletions(-) create mode 100644 scripts/site_raser.py diff --git a/Makefile b/Makefile index 778f343..bc13e79 100644 --- a/Makefile +++ b/Makefile @@ -1109,6 +1109,19 @@ genome-pousser: ## Le runner du site pousse le genome sur la forge du site — D playbooks/maintenance/genome_pousser.yml "$${supp[@]}" $(ARGS) .PHONY: site-creer +site-raser: ansible-runtime ## DESTRUCTIF : detruit les VM du SITE — CONFIRMER=true ET SITE= + @# CETTE CIBLE MANQUAIT, ET SON ABSENCE AVAIT UN NOM (2026-09-12). `raser` ne vise que + @# l'instance ACTIVE — un locataire. Rien ne detruisait les machines du site : ce n'est + @# donc pas que personne n'avait essaye de le reconstruire depuis zero, c'est que + @# l'outil n'en offrait pas le moyen. La limite « le site n'a jamais ete reconstruit » + @# se lisait partout sans que sa CAUSE soit nommee. + @# + @# LE SITE PORTE LE DEPOT DE SAUVEGARDE DE CHAQUE LOCATAIRE et la racine de son AC. + @# `make depot-hors-site VERS=` AVANT, toujours. + python3 scripts/site_raser.py $(if $(filter true,$(CONFIRMER)),--confirmer) \ + $(if $(SITE),--site "$(SITE)") $(if $(MACHINE),--machine "$(MACHINE)") + +.PHONY: site-raser site-creer: ## Cree les machines du site depuis l'underlay — CONFIRMER=true @set -e; \ if [[ "$(CONFIRMER)" != "true" ]]; then \ diff --git a/docs/audit/preuve-2026-09-12.md b/docs/audit/preuve-2026-09-12.md index c7b5877..c7c24d9 100644 --- a/docs/audit/preuve-2026-09-12.md +++ b/docs/audit/preuve-2026-09-12.md @@ -33,7 +33,7 @@ | P18 | Gabarit de voute complet | AFF-026 | ✅ OK | Gabarit de voute complet : 23 secret(s) exige(s), tous presents. (Voute reelle non lisible ici : verification sautee.) | | P19 | Le GUI couvre le schema du plan | AFF-002, AFF-095 | ✅ OK | GUI : les 29 champ(s) des plans reels sont editables (2 plan(s) inspecte(s)), registres toleres : nomenclature. | | P20 | Adressage 100% derive du seed (aucun stocke) | AFF-001, AFF-003 | ✅ OK | 2 nomenclature(s) : adressage 100% derive du seed index. | -| P21 | Federation : aucun index en collision | AFF-102 | ✅ OK | Federation coherente : 4 instance(s) federee(s), aucun index en collision. | +| P21 | Federation : aucun index en collision | AFF-102 | ✅ OK | Federation coherente : 5 instance(s) federee(s), aucun index en collision. | | P22 | Plan de recette a jour (genere du wiki) | AFF-002 | ✅ OK | Plan de recette à jour (22 sections). | | P23 | Underlay sans collision avec la plage tenant | AFF-103 | ✅ OK | Underlay conforme : 13 reseau(x), aucune collision avec la plage tenant. | | P24 | Frontiere nord/sud : acces d'administration declare | AFF-104 | ✅ OK | note : serveur_powerdns declare un port `derive` que le plan du site ne resout pas — aucune regle emise. | @@ -43,7 +43,7 @@ | P28 | Pools Proxmox : un par tenant, sans collision | AFF-110 | ✅ OK | CONFORME : 3 pool(s) Proxmox, 34 VM placee(s), aucun nom ni VMID en collision. | | P29 | Authentification : chaque role declare sa position | AFF-111 | ✅ OK | 33 role(s) serveur declares (interne-sans-auth 2, ldap-direct 2, sans-auth-humaine 22, socle-identite 2, web-sso 5) ; 2 lacune(s) nommee(s) : serveur_loki, serv | | P30 | SDN EVPN : zones, VNets et sous-reseaux derives | AFF-112 | ✅ OK | CONFORME : SDN EVPN, 3 zone(s), 15 VNet(s), 15 sous-reseau(x), aucune collision. | -| P31 | Documentation : tout ce que le depot FAIT est nomme | — | ✅ OK | 60 scripts expliques et atteignables, 117 cibles make documentees, 68 roles avec README. | +| P31 | Documentation : tout ce que le depot FAIT est nomme | — | ✅ OK | 61 scripts expliques et atteignables, 118 cibles make documentees, 68 roles avec README. | | P32 | Intrants exiges par les roles : tous fournis | — | ✅ OK | CONFORME : 38 exigence(s) de role, toutes satisfaites (138 cle(s) declaree(s) par l'instance). | | P33 | Aucune collision de port entre roles co-localises | — | ✅ OK | CONFORME : 34 revendication(s) de port, aucune collision entre roles co-localises (36 groupes). | | P34 | Chaque document declare son lecteur | — | ✅ OK | 44 document(s) declarent leur lecteur (38 genere(s) exempte(s)). | @@ -53,7 +53,7 @@ | P38 | Catalogue des services : la carte dit ce que le moteur fait | — | ✅ OK | Catalogue a jour : 41 role(s) serveur/client tous nommes, 41 groupe(s) cite(s) en table existent tous. | | P39 | Glossaire : tout mot employe est enseigne | — | ✅ OK | Glossaire complet : 81 terme(s) du jargon expliques, 15 lien(s) valides, 27 page(s) de wiki toutes atteignables. | | P40 | Parente : l'ecosysteme sait de quoi il descend | — | ✅ OK | Parente coherente : 4 depot(s), tous retrouves, tous porteurs d'un remote. | -| P41 | Resolution d'instance : une seule, partagee | — | ✅ OK | Resolution unique : 56 script(s) passent par `inventory_rules`, 3 exemption(s) nommee(s). | +| P41 | Resolution d'instance : une seule, partagee | — | ✅ OK | Resolution unique : 57 script(s) passent par `inventory_rules`, 3 exemption(s) nommee(s). | | P42 | L'edge porte les noms qu'il publie | — | ✅ OK | 4 edge(s) emettent un certificat portant les noms publies (OPS-Chezlepro-lab/principal, OPS-Chezlepro/principal, OPS-Technolibre/principal, OPS-Patient0/product | | P43 | Frontiere : le devis voit les machines du site | — | ✅ OK | Devis de la frontiere : 7 machine(s) du plan retrouvees, 151 regle(s) du site. | | P44 | Integrations : le serveur avant ses clients | — | ✅ OK | 5 integration(s) appliquent leur serveur avant leurs clients. | @@ -72,7 +72,7 @@ | P57 | Comptes en prose : les chiffres du depot sur lui-meme | — | ✅ OK | Les comptes ecrits en prose correspondent a la mesure (68 preuves, 68 roles, 41 groupes). | | P58 | Habilitations : chaque service dit a quel GROUPE, et par quoi | — | ✅ OK | 8 habilitation(s) declarees, toutes nommant un groupe, un mecanisme connu et une raison ; les `role-realm` sont projetees. | | P59 | Enumerations annoncees : le nombre correspond a ce qui suit | — | ✅ OK | 2 enumeration(s) annoncee(s) correspondent a ce qu'elles annoncent (formes non ambigues seulement). | -| P60 | Wiki publie : la forge sert ce que le depot dit | AFF-002 | ✅ OK | Le wiki publie correspond au depot : `wiki/` n'a pas bouge depuis `989398f` (publie le 2026-09-11). | +| P60 | Wiki publie : la forge sert ce que le depot dit | AFF-002 | ✅ OK | Le wiki publie correspond au depot : `wiki/` n'a pas bouge depuis `904ece3` (publie le 2026-09-12). | | P61 | Schema du plan : il decrit tout ce que les plans contiennent | AFF-033 | ✅ OK | Le schema decrit 47 champ(s) sur 6 registres ; il couvre tout ce que les plans reels contiennent, et la FORME de chaque champ (scalaire / objet / table) corresp | | P62 | Schema du plan : il decrit tout ce que le MOTEUR accepte | AFF-033 | ✅ OK | Les 4 validateurs n'acceptent aucun champ que le schema ignore (applications:8, bases_donnees:4, domaines_publics:5, serveurs:3 champ(s) lus par validateur). | | P63 | cloud-init nait avec la VM et ne lui survit pas | — | ✅ OK | cloud-init est au gabarit (la premiere seconde), absent du socle (pas de va-et-vient), et retire par le durcissement — avec la garde qui verifie que le reseau s | diff --git a/scripts/site_raser.py b/scripts/site_raser.py new file mode 100644 index 0000000..ae921e8 --- /dev/null +++ b/scripts/site_raser.py @@ -0,0 +1,165 @@ +#!/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())