Set-OPS-Public/scripts/tests/test_ecriture_atomique.py

84 lines
3.4 KiB
Python
Raw Normal View History

plan : l ecriture des registres devient atomique — tout, ou rien `path.open("w")` TRONQUE avant d ecrire : entre les deux, le fichier est vide. Une exception dans yaml.safe_dump, un disque plein, un Ctrl-C, et instance/plan/serveurs.yml reste mutile. L asymetrie fait la gravite : hosts.yml se regenere d un make instancier-appliquer, le PLAN ne se regenere de rien. C est la source unique de verite. Git est le filet, mais encore faut-il savoir qu on est tombe. TREIZE SITES, UNE SEULE FONCTION Le defaut n etait pas dans le GUI seul : douze sites dans sept fichiers, dont les miroirs CLI des MEMES registres. Corriger le GUI seul aurait recree la divergence que P41 garde depuis les neuf resolutions d instance. La fonction vit donc dans inventory_rules.py, que les sept importaient deja. Une source, pas douze. TROIS DETAILS QUI FONT LA DIFFERENCE ENTRE « CA MARCHE » ET « CA TIENT » temporaire dans le MEME dossier os.replace n est atomique qu au sein d un meme systeme de fichiers ; un /tmp sur une autre partition casserait la garantie sans rien dire fsync AVANT le rename sinon le renommage peut atteindre le disque avant le contenu : au retour d une coupure brutale, un fichier neuf et VIDE — le defaut qu on ferme, deplace d un cran report des droits mkstemp cree en 0600, le plan est en 0664 et doit rester lisible par le groupe sur les runners LE TEST PORTE SON PROPRE CONTROLE NEGATIF scripts/tests/test_ecriture_atomique.py rejoue D ABORD l ancienne forme et verifie qu elle DETRUIT. Sans ce controle, « le fichier est intact » ne prouverait rien — il pourrait l etre parce que rien n a ete ecrit du tout. Une garantie qu on n a jamais vue echouer n est pas une garantie, c est une habitude. Branche sur P02, dont le titre annoncait « inventory_host » alors qu il lance maintenant trois tests. Corrige au passage. LA VOUTE DU GUI : VERIFIEE, PAS DE DEFAUT Le soupcon etait qu executer_flux pose ANSIBLE_VAULT_PASSWORD_FILE (un seul mot de passe) alors que creer une VM ouvre DEUX voutes depuis « une voute, une cle ». Eprouve contre deux voutes JETABLES a mots de passe distincts — jamais les vraies. Les deux variables se CUMULENT : Ansible essaie tous les secrets, et un PASSWORD_FILE errone n empeche rien. Confirme en sondant l environnement qu une recette make recoit reellement : le mot de passe saisi ET les cinq cles calculees par voutes.py. Ce qui sauve ce chemin n est donc pas le mot de passe saisi, c est l IDENTITY_LIST que make pose par-dessus. Chacun couvre ce que l autre ne couvre pas — le PASSWORD_FILE sert le runner qui n a que sa cle, l IDENTITY_LIST le poste qui les a toutes. Ecrit au-dessus du code, pour que personne ne « simplifie » en retirant l un des deux. make prouver : CONFORME, 59 OK, 0 echec, 1 saute. make instancier : DIFF VIDE, quatre registres relus, droits 664 preserves. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 14:57:57 -04:00
"""L'ecriture du plan est ATOMIQUE : une panne en cours d'ecriture ne tronque rien.
POURQUOI CE TEST EXISTE. Jusqu'au 2026-09-08, treize sites du moteur ecrivaient les
registres par `path.open("w")` — une forme qui TRONQUE le fichier avant d'ecrire. Entre
les deux, il est vide. Une exception dans `yaml.safe_dump`, un disque plein, un Ctrl-C, et
`instance/plan/serveurs.yml` reste tronque.
L'asymetrie fait la gravite : `hosts.yml` se regenere d'un `make instancier-appliquer`, le
PLAN ne se regenere de rien. C'est la source unique de verite.
CE TEST PORTE SON PROPRE CONTROLE NEGATIF, et c'est le point. Il rejoue d'abord l'ANCIENNE
forme pour montrer qu'elle DETRUIT — sans quoi le succes de la nouvelle ne prouverait
rien. Une garantie qu'on n'a jamais vue echouer n'est pas une garantie, c'est une
habitude.
Aucun fichier reel n'est touche : tout se passe dans un dossier temporaire.
"""
import pathlib
import sys
import tempfile
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1]))
from inventory_rules import ecriture_atomique # noqa: E402
CONTENU = "serveurs:\n infra-pki-01:\n fonction: infra-pki\n"
def _neuf() -> pathlib.Path:
d = pathlib.Path(tempfile.mkdtemp(prefix="setops-atomique-"))
f = d / "serveurs.yml"
f.write_text(CONTENU, encoding="utf-8")
return f
# --- CONTROLE NEGATIF : l'ancienne forme detruit-elle vraiment ? -----------------------
f = _neuf()
try:
with f.open("w", encoding="utf-8") as fichier:
fichier.write("moitie...")
raise RuntimeError("panne simulee au milieu de l'ecriture")
except RuntimeError:
pass
perdu = f.read_text(encoding="utf-8")
print(f">>> ancienne forme, apres panne : {perdu!r}")
assert perdu != CONTENU, "LE CONTROLE NE CONTROLE RIEN : l'ancienne forme n'a rien casse"
print(">>> le controle mord : `open(\"w\")` laisse bien un fichier mutile")
# --- LA GARANTIE : la nouvelle forme survit-elle a la meme panne ? ---------------------
f = _neuf()
try:
with ecriture_atomique(f) as fichier:
fichier.write("moitie...")
raise RuntimeError("panne simulee au milieu de l'ecriture")
except RuntimeError:
pass
assert f.read_text(encoding="utf-8") == CONTENU, "LE FICHIER A ETE TRONQUE"
print(">>> apres la meme panne, le fichier est INTACT")
# Aucun residu : un `.serveurs.yml.xxxx.tmp` oublie finirait par etre committe.
restes = [p.name for p in f.parent.iterdir() if p.name != f.name]
assert not restes, f"RESIDU LAISSE DERRIERE : {restes}"
print(">>> aucun temporaire laisse dans le dossier")
# --- L'ecriture normale remplace bien le contenu ---------------------------------------
f = _neuf()
with ecriture_atomique(f) as fichier:
fichier.write("serveurs: {}\n")
assert f.read_text(encoding="utf-8") == "serveurs: {}\n", "L'ECRITURE NORMALE A ECHOUE"
print(">>> l'ecriture normale remplace le contenu")
# --- LES DROITS SURVIVENT ---------------------------------------------------------------
# `mkstemp` cree en 0600 ; le plan est en 0664, lisible par le groupe sur les runners.
# Sans report explicite des droits, le correctif rendait le plan illisible pour eux.
f = _neuf()
f.chmod(0o664)
with ecriture_atomique(f) as fichier:
fichier.write(CONTENU)
mode = f.stat().st_mode & 0o777
assert mode == 0o664, f"DROITS PERDUS : {oct(mode)} au lieu de 0o664"
print(">>> les droits du fichier sont conserves (0664)")
print("\n>>> l'ecriture du plan est atomique, eprouvee contre son propre controle.")