make raser : la seule commande destructive du moteur, avec ses quatre verrous
Ajoutee pour rendre la reconstruction from-zero REPETABLE — un test qu'on ne peut jouer qu'une fois n'est pas une recette. Verrous, tous eprouves avant usage : - VMID derives du plan uniquement (le gabarit dore est structurellement exclu) - le NOM doit correspondre : un VMID du plan sous un autre nom fait refuser l'operation ENTIERE. Pas theorique — le 2026-08-07 une VM heritee portait un VMID du plan sous le nom web-frontal-01 et proxmox_kvm rapportait ok. - il faut NOMMER l'ecosysteme (INSTANCE=) : le symlink instance/ peut pointer n'importe ou ; taper le nom distingue le POC de la production. - CONFIRMER=true ; sans lui, inventaire et rien d'autre. Le verrou du nom est le seul qu'on ne peut pas eprouver sur le vrai cluster sans y fabriquer une collision : scripts/tests/test_raser.py l'isole derriere un faux cluster, rattache a P02. Harnais : 31 preuves, 0 sautee. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
7dcbc6a0a4
commit
a430edf014
6 changed files with 216 additions and 6 deletions
25
CHANGELOG.md
25
CHANGELOG.md
|
|
@ -1,5 +1,30 @@
|
||||||
# CHANGELOG — Set-OPS
|
# CHANGELOG — Set-OPS
|
||||||
|
|
||||||
|
## 2026-08-08 — `make raser` : la seule commande destructive du moteur
|
||||||
|
|
||||||
|
Ajoutée pour rendre la reconstruction from-zero **répétable** — un test qu'on ne peut jouer
|
||||||
|
qu'une fois, à la main, n'est pas une recette. Tout le reste du dépôt crée ou réconcilie ;
|
||||||
|
celle-ci détruit, et elle est écrite en conséquence.
|
||||||
|
|
||||||
|
**Quatre verrous, tous éprouvés avant usage :**
|
||||||
|
|
||||||
|
| Verrou | Ce qu'il empêche | Vérifié |
|
||||||
|
|---|---|---|
|
||||||
|
| VMID **dérivés du plan** uniquement | détruire une VM hors écosystème ; le gabarit doré est structurellement exclu, son VMID ne se dérive pas | à blanc : 14 VM listées, template absent |
|
||||||
|
| **le nom doit correspondre** | détruire la machine de quelqu'un d'autre sous un VMID du plan | test isolé, faux cluster |
|
||||||
|
| **nommer l'écosystème** (`INSTANCE=`) | raser la mauvaise instance : le symlink `instance/` peut pointer n'importe où | refus sans nom, et refus sur mauvais nom |
|
||||||
|
| `CONFIRMER=true` | tout le reste | sans lui : inventaire, rien d'autre |
|
||||||
|
|
||||||
|
Le deuxième mérite d'être détaillé, parce qu'il vient d'un fait et non d'une précaution
|
||||||
|
abstraite : le 2026-08-07, une VM héritée portait un VMID du plan sous le nom
|
||||||
|
`web-frontal-01`, et `proxmox_kvm` avait rapporté `ok` sans rien faire. Rasé sans ce
|
||||||
|
contrôle, on détruisait une machine étrangère. Le refus porte sur **l'opération entière**,
|
||||||
|
pas sur la seule VM en conflit — un cluster qui ment sur un VMID peut mentir sur d'autres.
|
||||||
|
|
||||||
|
C'est aussi le seul verrou qu'on ne peut pas éprouver sur le vrai cluster sans y fabriquer
|
||||||
|
une collision : `scripts/tests/test_raser.py` isole la logique derrière un faux cluster, et
|
||||||
|
le test est rattaché à **P02**. Le harnais passe désormais **31 preuves, 0 sautée**.
|
||||||
|
|
||||||
## 2026-08-08 — État de référence figé avant la reconstruction from-zero
|
## 2026-08-08 — État de référence figé avant la reconstruction from-zero
|
||||||
|
|
||||||
L'exploitant recadre : les 14 VM sont un **POC**, pas de la production. Ma prudence venait
|
L'exploitant recadre : les 14 VM sont un **POC**, pas de la production. Ma prudence venait
|
||||||
|
|
|
||||||
4
Makefile
4
Makefile
|
|
@ -209,6 +209,7 @@ syntaxe-proxmox: ansible-runtime ## Verifie la syntaxe du playbook de clonage de
|
||||||
.PHONY: test
|
.PHONY: test
|
||||||
test: ## Lance les tests unitaires (derivation de nomenclature et d'inventaire)
|
test: ## Lance les tests unitaires (derivation de nomenclature et d'inventaire)
|
||||||
python3 scripts/tests/test_inventory_host.py
|
python3 scripts/tests/test_inventory_host.py
|
||||||
|
python3 scripts/tests/test_raser.py
|
||||||
|
|
||||||
.PHONY: verifier
|
.PHONY: verifier
|
||||||
verifier: lint test inventaire-verifier site-verifier flux-verifier syntaxe ## Rejoue les preuves SANS reecrire le rapport (verification rapide)
|
verifier: lint test inventaire-verifier site-verifier flux-verifier syntaxe ## Rejoue les preuves SANS reecrire le rapport (verification rapide)
|
||||||
|
|
@ -580,6 +581,9 @@ _attendre-flotte: ansible-runtime
|
||||||
printf 'Flotte joignable.\n'
|
printf 'Flotte joignable.\n'
|
||||||
|
|
||||||
.PHONY: reconstruire
|
.PHONY: reconstruire
|
||||||
|
raser: ansible-runtime _instance-requise ## DESTRUCTIF : detruit les VM derivees du plan — exige CONFIRMER=true ET INSTANCE=<nom>
|
||||||
|
@python3 scripts/raser.py $(if $(INSTANCE),--instance $(INSTANCE)) $(if $(filter true,$(CONFIRMER)),--confirmer)
|
||||||
|
|
||||||
reconstruire: _instance-requise ## Reconstruit un ecosysteme depuis zero : VM puis deploiement complet
|
reconstruire: _instance-requise ## Reconstruit un ecosysteme depuis zero : VM puis deploiement complet
|
||||||
@set -e; \
|
@set -e; \
|
||||||
if [[ "$(CONFIRMER)" != "true" ]]; then \
|
if [[ "$(CONFIRMER)" != "true" ]]; then \
|
||||||
|
|
|
||||||
|
|
@ -7,14 +7,14 @@
|
||||||
> [`docs/audit/affirmations.md`](affirmations.md).
|
> [`docs/audit/affirmations.md`](affirmations.md).
|
||||||
|
|
||||||
- **Instance** : `instance` — inventaire `instance/inventories/principal/hosts.yml`
|
- **Instance** : `instance` — inventaire `instance/inventories/principal/hosts.yml`
|
||||||
- **Verdict** : ✅ CONFORME (30 OK · 0 echec · 1 saute)
|
- **Verdict** : ✅ CONFORME (31 OK · 0 echec · 0 saute)
|
||||||
|
|
||||||
## Preuves
|
## Preuves
|
||||||
|
|
||||||
| # | Preuve | Affirmations | Statut | Detail |
|
| # | Preuve | Affirmations | Statut | Detail |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| P01 | Lint (ansible-lint) | AFF-006 | ✅ OK | [0m[0m |
|
| P01 | Lint (ansible-lint) | AFF-006 | ✅ OK | [0m[0m |
|
||||||
| P02 | Tests unitaires (inventory_host) | — | ✅ OK | 5 tests passes. |
|
| P02 | Tests unitaires (inventory_host) | — | ✅ OK | >>> le verrou tient : aucune VM n'aurait ete touchee |
|
||||||
| P03 | Diff-vide du plan (inventaire genere) | AFF-001, AFF-004, AFF-030, AFF-031, AFF-032 | ✅ OK | DIFF VIDE : le plan reproduit exactement l'inventaire actuel. Bascule possible. |
|
| P03 | Diff-vide du plan (inventaire genere) | AFF-001, AFF-004, AFF-030, AFF-031, AFF-032 | ✅ OK | DIFF VIDE : le plan reproduit exactement l'inventaire actuel. Bascule possible. |
|
||||||
| P04 | Groupes <-> playbooks homonymes | AFF-008 | ✅ OK | |
|
| P04 | Groupes <-> playbooks homonymes | AFF-008 | ✅ OK | |
|
||||||
| P05 | Dependances causales de groupes | AFF-009, AFF-084 | ✅ OK | |
|
| P05 | Dependances causales de groupes | AFF-009, AFF-084 | ✅ OK | |
|
||||||
|
|
@ -28,9 +28,9 @@
|
||||||
| P13 | Invariants structurels/doctrinaux | AFF-015, AFF-022, AFF-037, AFF-038, AFF-062 | ✅ OK | LICENSE, socle dossier, pas de couches paralleles, SSH clef-only, nftables off : OK. |
|
| P13 | Invariants structurels/doctrinaux | AFF-015, AFF-022, AFF-037, AFF-038, AFF-062 | ✅ OK | LICENSE, socle dossier, pas de couches paralleles, SSH clef-only, nftables off : OK. |
|
||||||
| P14 | Pas de chemin lab/ code en dur | AFF-097 | ✅ OK | Aucun chemin instance/inventories/lab/group_vars code en dur. |
|
| P14 | Pas de chemin lab/ code en dur | AFF-097 | ✅ OK | Aucun chemin instance/inventories/lab/group_vars code en dur. |
|
||||||
| P15 | Modele public socle valide | AFF-022, AFF-099 | ✅ OK | Modele public socle : domaines/serveurs/applications/bases valides. |
|
| P15 | Modele public socle valide | AFF-022, AFF-099 | ✅ OK | Modele public socle : domaines/serveurs/applications/bases valides. |
|
||||||
| P16 | Inventaire Ansible complet (--list) | AFF-030 | ⚪ SAUTE | Voute chiffree sans ANSIBLE_VAULT_PASSWORD_FILE (prerequis AFF-026). |
|
| P16 | Inventaire Ansible complet (--list) | AFF-030 | ✅ OK | 14 hotes, 31 groupes (inventaire dechiffre et parse). |
|
||||||
| P17 | Tous les modeles valident (registres + underlay) | AFF-022, AFF-099 | ✅ OK | Les 1 modele(s) decouvert(s) valident. |
|
| P17 | Tous les modeles valident (registres + underlay) | AFF-022, AFF-099 | ✅ OK | Les 1 modele(s) decouvert(s) valident. |
|
||||||
| P18 | Gabarit de voute complet | AFF-026 | ✅ OK | Gabarit de voute complet : 25 secret(s) exige(s), tous presents. (Voute reelle non lisible ici : verification sautee.) |
|
| P18 | Gabarit de voute complet | AFF-026 | ✅ OK | Gabarit de voute complet : 25 secret(s) exige(s), tous presents. Voute reelle : 28 cle(s), aucun manque. |
|
||||||
| P19 | Le GUI couvre le schema du plan | AFF-002, AFF-095 | ✅ OK | GUI : les 28 champ(s) des plans reels sont editables (2 plan(s) inspecte(s)), registres toleres : nomenclature. |
|
| P19 | Le GUI couvre le schema du plan | AFF-002, AFF-095 | ✅ OK | GUI : les 28 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. |
|
| 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 : 2 instance(s) federee(s), aucun index en collision. |
|
| P21 | Federation : aucun index en collision | AFF-102 | ✅ OK | Federation coherente : 2 instance(s) federee(s), aucun index en collision. |
|
||||||
|
|
@ -43,7 +43,7 @@
|
||||||
| P28 | Pools Proxmox : un par tenant, sans collision | AFF-110 | ✅ OK | CONFORME : 2 pool(s) Proxmox, 28 VM placee(s), aucun nom ni VMID en collision. |
|
| P28 | Pools Proxmox : un par tenant, sans collision | AFF-110 | ✅ OK | CONFORME : 2 pool(s) Proxmox, 28 VM placee(s), aucun nom ni VMID en collision. |
|
||||||
| P29 | Authentification : chaque role declare sa position | AFF-111 | ✅ OK | 23 role(s) serveur declares (interne-sans-auth 2, ldap-direct 2, sans-auth-humaine 12, socle-identite 2, web-sso 5) ; 2 lacune(s) nommee(s) : serveur_loki, serv |
|
| P29 | Authentification : chaque role declare sa position | AFF-111 | ✅ OK | 23 role(s) serveur declares (interne-sans-auth 2, ldap-direct 2, sans-auth-humaine 12, 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, 2 zone(s), 12 VNet(s), 12 sous-reseau(x), aucune collision. |
|
| P30 | SDN EVPN : zones, VNets et sous-reseaux derives | AFF-112 | ✅ OK | CONFORME : SDN EVPN, 2 zone(s), 12 VNet(s), 12 sous-reseau(x), aucune collision. |
|
||||||
| P31 | Documentation : tout ce que le depot FAIT est nomme | — | ✅ OK | 35 scripts expliques et atteignables, 85 cibles make documentees, 54 roles avec README. |
|
| P31 | Documentation : tout ce que le depot FAIT est nomme | — | ✅ OK | 36 scripts expliques et atteignables, 86 cibles make documentees, 54 roles avec README. |
|
||||||
|
|
||||||
## Couverture des affirmations ✅ du registre
|
## Couverture des affirmations ✅ du registre
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -496,7 +496,8 @@ PREUVES: list[dict] = [
|
||||||
{"id": "P01", "titre": "Lint (ansible-lint)", "refs": ["AFF-006"],
|
{"id": "P01", "titre": "Lint (ansible-lint)", "refs": ["AFF-006"],
|
||||||
"cmds": [["ansible-lint", "-q"]]},
|
"cmds": [["ansible-lint", "-q"]]},
|
||||||
{"id": "P02", "titre": "Tests unitaires (inventory_host)", "refs": [],
|
{"id": "P02", "titre": "Tests unitaires (inventory_host)", "refs": [],
|
||||||
"cmds": [[sys.executable, "scripts/tests/test_inventory_host.py"]]},
|
"cmds": [[sys.executable, "scripts/tests/test_inventory_host.py"],
|
||||||
|
[sys.executable, "scripts/tests/test_raser.py"]]},
|
||||||
{"id": "P03", "titre": "Diff-vide du plan (inventaire genere)", "refs": ["AFF-001", "AFF-004", "AFF-030", "AFF-031", "AFF-032"],
|
{"id": "P03", "titre": "Diff-vide du plan (inventaire genere)", "refs": ["AFF-001", "AFF-004", "AFF-030", "AFF-031", "AFF-032"],
|
||||||
"refs": ["AFF-001", "AFF-004", "AFF-030", "AFF-031", "AFF-032"],
|
"refs": ["AFF-001", "AFF-004", "AFF-030", "AFF-031", "AFF-032"],
|
||||||
"cmds": [[sys.executable, "scripts/instancier.py", "generer"],
|
"cmds": [[sys.executable, "scripts/instancier.py", "generer"],
|
||||||
|
|
|
||||||
150
scripts/raser.py
Executable file
150
scripts/raser.py
Executable file
|
|
@ -0,0 +1,150 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Rase les VM d'un ecosysteme — la seule commande destructive du moteur.
|
||||||
|
|
||||||
|
Elle n'existe que pour rendre la reconstruction from-zero REPETABLE : un test qu'on ne
|
||||||
|
peut jouer qu'une fois, a la main, n'est pas une recette. Tout le reste du depot cree ou
|
||||||
|
reconcilie ; celle-ci detruit, et elle est ecrite en consequence.
|
||||||
|
|
||||||
|
QUATRE VERROUS, et aucun n'est cosmetique :
|
||||||
|
|
||||||
|
1. Ne touche QUE les VMID **derives du plan** de l'instance active. Une VM qui n'est
|
||||||
|
pas au plan n'est jamais candidate — le gabarit dore en particulier, dont le VMID
|
||||||
|
ne se derive pas.
|
||||||
|
2. **Le nom doit correspondre.** Un VMID present sous un AUTRE nom que celui du plan
|
||||||
|
est la machine de quelqu'un d'autre : l'operation entiere est refusee, pas
|
||||||
|
seulement cette VM. Ce n'est pas theorique — le 2026-08-07, une VM heritee portait
|
||||||
|
un VMID du plan sous le nom `web-frontal-01`, et `proxmox_kvm` avait rapporte `ok`
|
||||||
|
sans rien faire.
|
||||||
|
3. **Il faut NOMMER l'ecosysteme** (`INSTANCE=<nom>`) et que ce nom corresponde a
|
||||||
|
l'instance montee. Le symlink `instance/` peut pointer n'importe ou ; taper le nom
|
||||||
|
est ce qui distingue « je rase le POC » de « je rase la production ».
|
||||||
|
4. `CONFIRMER=true`. Sans lui : inventaire de ce qui serait detruit, et rien d'autre.
|
||||||
|
|
||||||
|
Usage :
|
||||||
|
python3 scripts/raser.py # dit ce qu'il ferait
|
||||||
|
python3 scripts/raser.py --instance chezlepro --confirmer
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||||
|
from proxmox_api import Cluster # noqa: E402
|
||||||
|
|
||||||
|
RACINE = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
|
||||||
|
def instance_active() -> tuple[Path, str]:
|
||||||
|
"""Dossier de l'instance montee, et son nom court (celui qu'il faudra taper)."""
|
||||||
|
base = Path(os.environ.get("SETOPS_INSTANCE") or (RACINE / "instance"))
|
||||||
|
if not base.exists():
|
||||||
|
raise SystemExit("Aucune instance montee : rien a raser.")
|
||||||
|
reel = base.resolve()
|
||||||
|
nom = reel.name.removeprefix("OPS-").lower()
|
||||||
|
return reel, nom
|
||||||
|
|
||||||
|
|
||||||
|
def plan_derive(base: Path) -> list[tuple[str, int]]:
|
||||||
|
"""(nom d'hote, vmid) tels que le PLAN les derive — la seule source légitime."""
|
||||||
|
env = dict(os.environ, SETOPS_INSTANCE=str(base))
|
||||||
|
r = subprocess.run([sys.executable, str(RACINE / "scripts" / "serveurs.py"), "lister"],
|
||||||
|
capture_output=True, text=True, env=env, cwd=RACINE)
|
||||||
|
if r.returncode != 0:
|
||||||
|
raise SystemExit(f"Plan illisible : {r.stderr.strip()[:200]}")
|
||||||
|
couples = []
|
||||||
|
for ligne in r.stdout.splitlines():
|
||||||
|
if "vmid" not in ligne:
|
||||||
|
continue
|
||||||
|
nom = ligne.split()[0]
|
||||||
|
for morceau in ligne.split():
|
||||||
|
if morceau.rstrip(",").isdigit() and len(morceau.rstrip(",")) >= 6:
|
||||||
|
couples.append((nom, int(morceau.rstrip(","))))
|
||||||
|
break
|
||||||
|
return couples
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str] | None = None) -> int:
|
||||||
|
ap = argparse.ArgumentParser(description="Rase les VM derivees du plan de l'instance active.")
|
||||||
|
ap.add_argument("--instance", help="nom de l'ecosysteme a raser (doit correspondre au monte)")
|
||||||
|
ap.add_argument("--confirmer", action="store_true")
|
||||||
|
a = ap.parse_args(argv)
|
||||||
|
|
||||||
|
base, nom_actif = instance_active()
|
||||||
|
couples = plan_derive(base)
|
||||||
|
if not couples:
|
||||||
|
print("Le plan ne derive aucune VM : 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 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"Écosystème monté : « {nom_actif} » ({base})\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} le plan dit « {nom} », le cluster dit « {autre} »")
|
||||||
|
|
||||||
|
if usurpees:
|
||||||
|
print(f"\nREFUS : {len(usurpees)} VMID du plan portent un AUTRE nom sur le cluster.\n"
|
||||||
|
f"Ce sont des machines qui ne nous appartiennent pas. Les renommer ou corriger\n"
|
||||||
|
f"le plan avant de raser quoi que ce soit — aucune VM n'a ete touchee.")
|
||||||
|
return 2
|
||||||
|
|
||||||
|
if not presentes:
|
||||||
|
print("\nAucune VM du plan n'est presente : rien a faire.")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if not a.confirmer:
|
||||||
|
print(f"\n{len(presentes)} VM seraient DETRUITES, disques compris.\n"
|
||||||
|
f"Relancer avec : make raser CONFIRMER=true INSTANCE={nom_actif}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if (a.instance or "").lower() != nom_actif:
|
||||||
|
print(f"\nREFUS : il faut NOMMER l'ecosysteme a raser.\n"
|
||||||
|
f"Monte : « {nom_actif} » ; recu : « {a.instance or '(rien)'} ».\n"
|
||||||
|
f"Taper le nom est ce qui distingue le POC de la production.")
|
||||||
|
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
|
||||||
|
else:
|
||||||
|
print(f" {vmid} {nom} — detruite")
|
||||||
|
|
||||||
|
print(f"\n{len(presentes) - echecs}/{len(presentes)} VM detruites."
|
||||||
|
+ (f" {echecs} echec(s)." if echecs else ""))
|
||||||
|
return 1 if echecs else 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
30
scripts/tests/test_raser.py
Normal file
30
scripts/tests/test_raser.py
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
"""`raser` : un VMID du plan portant un AUTRE nom doit faire REFUSER l'operation entiere.
|
||||||
|
|
||||||
|
C'est le verrou le plus important de la seule commande destructive du moteur, et le seul
|
||||||
|
qu'on ne peut pas eprouver sur le vrai cluster sans y fabriquer une collision. On isole
|
||||||
|
donc la logique derriere un faux cluster.
|
||||||
|
|
||||||
|
Le danger n'est pas theorique : le 2026-08-07, une VM heritee portait un VMID du plan sous
|
||||||
|
le nom `web-frontal-01`, et `proxmox_kvm` avait rapporte `ok` sans rien faire. Rase sans ce
|
||||||
|
controle, on detruisait la machine de quelqu'un d'autre.
|
||||||
|
"""
|
||||||
|
import sys, pathlib
|
||||||
|
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
||||||
|
import raser
|
||||||
|
|
||||||
|
class FauxCluster:
|
||||||
|
"""Le cluster repond que 117603101 s'appelle « vm-heritee-de-2019 »."""
|
||||||
|
@classmethod
|
||||||
|
def depuis_hebergeur(cls):
|
||||||
|
return cls(), None
|
||||||
|
def __call__(self, chemin, methode="GET", corps=None):
|
||||||
|
return [{"type": "qemu", "vmid": 117603101, "name": "vm-heritee-de-2019", "node": "asgard"},
|
||||||
|
{"type": "qemu", "vmid": 117101101, "name": "infra-edge-01", "node": "asgard"}]
|
||||||
|
def rate(self, rep):
|
||||||
|
return None
|
||||||
|
|
||||||
|
raser.Cluster = FauxCluster
|
||||||
|
code = raser.main(["--instance", "chezlepro", "--confirmer"])
|
||||||
|
print(f"\n>>> code de sortie : {code} (2 = refus attendu)")
|
||||||
|
assert code == 2, "LE VERROU N'A PAS TENU"
|
||||||
|
print(">>> le verrou tient : aucune VM n'aurait ete touchee")
|
||||||
Loading…
Reference in a new issue