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
This commit is contained in:
Daniel Allaire 2026-09-12 16:32:06 -04:00
parent 5419ee2d71
commit 0c55aafc40
3 changed files with 182 additions and 4 deletions

View file

@ -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=<depot>
@# 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=<repertoire>` 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 \

View file

@ -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 |

165
scripts/site_raser.py Normal file
View file

@ -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())