plan : l ecriture des registres devient atomique — tout, ou rien
Some checks are pending
verifier / verifier (push) Waiting to run
Some checks are pending
verifier / verifier (push) Waiting to run
`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
This commit is contained in:
parent
6da1032ceb
commit
6077b179af
12 changed files with 180 additions and 19 deletions
|
|
@ -14,7 +14,7 @@
|
|||
| # | Preuve | Affirmations | Statut | Detail |
|
||||
|---|---|---|---|---|
|
||||
| P01 | Lint (ansible-lint) | AFF-006 | ✅ OK | [0m[0m |
|
||||
| P02 | Tests unitaires (inventory_host) | — | ✅ OK | >>> le verrou tient : aucune VM n'aurait ete touchee |
|
||||
| P02 | Tests unitaires (inventaire, raser, ecriture atomique) | — | ✅ OK | >>> l'ecriture du plan est atomique, eprouvee contre son propre controle. |
|
||||
| P03 | Diff-vide du plan — TOUTES les instances | AFF-001, AFF-004, AFF-030, AFF-031, AFF-032 | ✅ OK | 4 instance(s) verifiee(s) — OPS-Chezlepro-lab, OPS-Chezlepro, OPS-Technolibre, OPS-Patient0 : plan et inventaire applique coincident. |
|
||||
| P04 | Groupes <-> playbooks homonymes | AFF-008 | ✅ OK | |
|
||||
| P05 | Dependances causales de groupes | AFF-009, AFF-084 | ✅ OK | |
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import yaml
|
|||
|
||||
from inventory_gui import charger_yaml, liste_hotes
|
||||
from inventory_rules import (
|
||||
ecriture_atomique,
|
||||
bases_de_application,
|
||||
chaine_connexion,
|
||||
charger_applications,
|
||||
|
|
@ -48,7 +49,7 @@ def ecrire(registre: dict) -> None:
|
|||
"# Une VM peut porter plusieurs applications ; une base se lie a une application.\n"
|
||||
"---\n"
|
||||
)
|
||||
with FICHIER.open("w", encoding="utf-8") as fichier:
|
||||
with ecriture_atomique(FICHIER) as fichier:
|
||||
fichier.write(entete)
|
||||
yaml.safe_dump({"applications": registre.get("applications", {}) or {}},
|
||||
fichier, default_flow_style=False, sort_keys=False, allow_unicode=True)
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ from pathlib import Path
|
|||
import yaml
|
||||
|
||||
from inventory_rules import (
|
||||
ecriture_atomique,
|
||||
instance_courante,
|
||||
chaine_connexion,
|
||||
charger_applications,
|
||||
|
|
@ -37,7 +38,7 @@ def ecrire(registre: dict) -> None:
|
|||
"serveurs_bd": registre.get("serveurs_bd", {}) or {},
|
||||
"bases_donnees": registre.get("bases_donnees", {}) or {},
|
||||
}
|
||||
with FICHIER.open("w", encoding="utf-8") as fichier:
|
||||
with ecriture_atomique(FICHIER) as fichier:
|
||||
fichier.write(entete)
|
||||
yaml.safe_dump(contenu, fichier, default_flow_style=False, sort_keys=False, allow_unicode=True)
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import sys
|
|||
|
||||
import yaml
|
||||
|
||||
from inventory_rules import instance_courante, inventaire_force
|
||||
from inventory_rules import ecriture_atomique, instance_courante, inventaire_force
|
||||
|
||||
|
||||
RACINE = Path(__file__).resolve().parents[1]
|
||||
|
|
@ -99,8 +99,7 @@ def charger_yaml(path: Path) -> dict:
|
|||
|
||||
|
||||
def ecrire_yaml(path: Path, data: dict) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with path.open("w", encoding="utf-8") as fichier:
|
||||
with ecriture_atomique(path) as fichier:
|
||||
fichier.write("---\n")
|
||||
fichier.write("# Parametres non sensibles pour les operations Proxmox.\n")
|
||||
fichier.write("# Les secrets vont dans la voûte unique group_vars/all/vault.yml.\n\n")
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ RACINE = Path(__file__).resolve().parents[1]
|
|||
sys.path.insert(0, str(RACINE / "scripts"))
|
||||
|
||||
import underlay as underlay_mod # noqa: E402
|
||||
from inventory_rules import instance_courante # noqa: E402
|
||||
from inventory_rules import ecriture_atomique, instance_courante # noqa: E402
|
||||
|
||||
FICHIER_PARENTE = "parente.yml"
|
||||
|
||||
|
|
@ -168,7 +168,7 @@ def inscrire() -> int:
|
|||
"# Il ne remplace pas les depots : il dit ou les retrouver et lequel etait le bon.\n"
|
||||
"---\n")
|
||||
cible = base / FICHIER_PARENTE
|
||||
with cible.open("w", encoding="utf-8") as f:
|
||||
with ecriture_atomique(cible) as f:
|
||||
f.write(entete)
|
||||
yaml.safe_dump(contenu, f, default_flow_style=False, sort_keys=False,
|
||||
allow_unicode=True)
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import yaml
|
|||
import underlay as underlay_mod # noqa: E402
|
||||
from devis_sdn import vnet_de # noqa: E402
|
||||
from inventory_rules import (
|
||||
ecriture_atomique,
|
||||
charger_applications,
|
||||
charger_domaines,
|
||||
charger_nomenclature,
|
||||
|
|
@ -324,7 +325,7 @@ def ecrire(path: Path = GENERE) -> None:
|
|||
entete = ("# Inventaire GENERE depuis le plan (make instancier / instancier-appliquer).\n"
|
||||
"# NE PAS editer a la main : edite instance/plan/serveurs.yml + instance/plan/applications.yml.\n"
|
||||
"# Source : instance/plan/serveurs.yml + instance/plan/applications.yml + instance/plan/nomenclature.yml.\n")
|
||||
with path.open("w", encoding="utf-8") as fichier:
|
||||
with ecriture_atomique(path) as fichier:
|
||||
fichier.write(entete)
|
||||
yaml.safe_dump(generer(), fichier, default_flow_style=False, sort_keys=True, allow_unicode=True)
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import devis_reseau
|
|||
|
||||
from inventory_rules import instance_courante # noqa: E402
|
||||
from inventory_rules import (
|
||||
ecriture_atomique,
|
||||
GROUPE_HOTES_ACTIFS,
|
||||
GROUPE_HOTES_PLANIFIES,
|
||||
GROUPES_ETAT_HOTE,
|
||||
|
|
@ -276,7 +277,7 @@ def charger_yaml(path: Path) -> dict:
|
|||
|
||||
|
||||
def ecrire_yaml(path: Path, data: dict) -> None:
|
||||
with path.open("w", encoding="utf-8") as fichier:
|
||||
with ecriture_atomique(path) as fichier:
|
||||
yaml.safe_dump(data, fichier, default_flow_style=False, sort_keys=False, allow_unicode=True)
|
||||
|
||||
|
||||
|
|
@ -695,7 +696,7 @@ def _ecrire_intrants_fichier(path: Path, valeurs: dict, entete: str) -> None:
|
|||
path.write_text(_fusion_chirurgicale(path.read_text(encoding="utf-8"), valeurs),
|
||||
encoding="utf-8")
|
||||
return
|
||||
with path.open("w", encoding="utf-8") as fichier:
|
||||
with ecriture_atomique(path) as fichier:
|
||||
fichier.write(entete)
|
||||
yaml.safe_dump(valeurs, fichier, default_flow_style=False, sort_keys=True, allow_unicode=True)
|
||||
|
||||
|
|
@ -846,7 +847,7 @@ def ecrire_bases(path: Path, registre: dict) -> None:
|
|||
"serveurs_bd": registre.get("serveurs_bd", {}) or {},
|
||||
"bases_donnees": registre.get("bases_donnees", {}) or {},
|
||||
}
|
||||
with path.open("w", encoding="utf-8") as fichier:
|
||||
with ecriture_atomique(path) as fichier:
|
||||
fichier.write(entete)
|
||||
yaml.safe_dump(contenu, fichier, default_flow_style=False, sort_keys=False, allow_unicode=True)
|
||||
|
||||
|
|
@ -860,7 +861,7 @@ def ecrire_applications(path: Path, registre: dict) -> None:
|
|||
"# les roles acceptes sont ceux de roles/<groupe>/meta/liens.yml.\n"
|
||||
"---\n"
|
||||
)
|
||||
with path.open("w", encoding="utf-8") as fichier:
|
||||
with ecriture_atomique(path) as fichier:
|
||||
fichier.write(entete)
|
||||
yaml.safe_dump({"applications": registre.get("applications", {}) or {}},
|
||||
fichier, default_flow_style=False, sort_keys=False, allow_unicode=True)
|
||||
|
|
@ -873,7 +874,7 @@ def ecrire_serveurs(path: Path, registre: dict) -> None:
|
|||
"# VMID/IP/VLAN/passerelle sont DERIVES de la fonction via instance/plan/nomenclature.yml.\n"
|
||||
"---\n"
|
||||
)
|
||||
with path.open("w", encoding="utf-8") as fichier:
|
||||
with ecriture_atomique(path) as fichier:
|
||||
fichier.write(entete)
|
||||
yaml.safe_dump({"serveurs": registre.get("serveurs", {}) or {}},
|
||||
fichier, default_flow_style=False, sort_keys=False, allow_unicode=True)
|
||||
|
|
@ -886,7 +887,7 @@ def ecrire_domaines(path: Path, registre: dict) -> None:
|
|||
"# autorite : primaire-cache | auto-heberge | delegue (label descriptif).\n"
|
||||
"---\n"
|
||||
)
|
||||
with path.open("w", encoding="utf-8") as fichier:
|
||||
with ecriture_atomique(path) as fichier:
|
||||
fichier.write(entete)
|
||||
yaml.safe_dump({"domaines_publics": registre.get("domaines_publics", {}) or {}},
|
||||
fichier, default_flow_style=False, sort_keys=False, allow_unicode=True)
|
||||
|
|
@ -2978,6 +2979,24 @@ class Gestionnaire(BaseHTTPRequestHandler):
|
|||
"creer": "creer-vm"}.get(mode, "verifier-deploiement")
|
||||
env = dict(os.environ, PYTHONUNBUFFERED="1", ANSIBLE_FORCE_COLOR="0")
|
||||
if vault:
|
||||
# LE MOT DE PASSE SAISI S'AJOUTE AUX CLES, IL NE LES REMPLACE PAS.
|
||||
#
|
||||
# Verifie empiriquement le 2026-09-08, contre deux voutes jetables a mots
|
||||
# de passe distincts : `ANSIBLE_VAULT_PASSWORD_FILE` et
|
||||
# `ANSIBLE_VAULT_IDENTITY_LIST` se CUMULENT — Ansible essaie tous les
|
||||
# secrets, et un PASSWORD_FILE errone n'empeche rien.
|
||||
#
|
||||
# Ca compte depuis « une voute, une cle » (2026-08-28) : creer une VM
|
||||
# ouvre DEUX voutes dans la meme execution — celle du tenant et
|
||||
# `underlay.vault.yml`. Un seul mot de passe ne pourrait pas les ouvrir
|
||||
# toutes les deux. Ce qui sauve ce chemin, c'est que `make` calcule
|
||||
# `ANSIBLE_VAULT_IDENTITY_LIST` par-dessus (Makefile, `?=`), et que les
|
||||
# deux coexistent dans l'environnement de la recette.
|
||||
#
|
||||
# NE PAS « SIMPLIFIER » EN RETIRANT L'UN DES DEUX. Le PASSWORD_FILE sert
|
||||
# le cas ou la cle n'est pas sur la machine (un runner qui n'a que la
|
||||
# sienne) ; l'IDENTITY_LIST sert le poste de l'exploitant, qui les a
|
||||
# toutes. Chacun couvre ce que l'autre ne couvre pas.
|
||||
fd, fichier_vault = tempfile.mkstemp(prefix="setops-vault-") # 0600 par defaut
|
||||
os.write(fd, str(vault).encode("utf-8"))
|
||||
os.close(fd)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import sys
|
|||
import yaml
|
||||
|
||||
from inventory_rules import (
|
||||
ecriture_atomique,
|
||||
variable_effective,
|
||||
GROUPE_HOTES_ACTIFS,
|
||||
GROUPE_HOTES_PLANIFIES,
|
||||
|
|
@ -35,7 +36,7 @@ def charger_inventaire(path: Path) -> dict:
|
|||
|
||||
|
||||
def ecrire_inventaire(path: Path, data: dict) -> None:
|
||||
with path.open("w", encoding="utf-8") as inventory_file:
|
||||
with ecriture_atomique(path) as inventory_file:
|
||||
yaml.safe_dump(
|
||||
data,
|
||||
inventory_file,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ from __future__ import annotations
|
|||
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
|
@ -22,6 +24,58 @@ ORDRE_INVENTAIRE = ("principal", "production")
|
|||
ORDRE_INVENTAIRE_MODELE = ("lab", "principal", "production")
|
||||
|
||||
|
||||
@contextmanager
|
||||
def ecriture_atomique(path: Path, encoding: str = "utf-8"):
|
||||
"""Ouvre `path` en ecriture de facon ATOMIQUE : tout, ou rien.
|
||||
|
||||
POURQUOI, ET POURQUOI ICI (2026-09-08). Douze sites du moteur ecrivaient le plan par
|
||||
`path.open("w")`. Cette forme 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 est ce qui rend le defaut grave : `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.
|
||||
|
||||
Le remede tient en deux gestes : ecrire dans un temporaire du MEME dossier, puis
|
||||
`os.replace()` — atomique sur POSIX (meme systeme de fichiers, donc simple rename).
|
||||
Le lecteur voit l'ancien fichier, puis le nouveau. Jamais rien entre les deux.
|
||||
|
||||
POSE DANS CE MODULE, ET PAS DANS CHAQUE APPELANT. Les sept ecrivains l'importent
|
||||
deja. Douze copies d'un meme geste ne vieillissent pas ensemble — c'est exactement la
|
||||
lecon des neuf resolutions d'instance que **P41** garde depuis. Une source, pas douze.
|
||||
|
||||
Usage :
|
||||
|
||||
with ecriture_atomique(chemin) as f:
|
||||
f.write(entete)
|
||||
yaml.safe_dump(donnees, f)
|
||||
"""
|
||||
path = Path(path)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
# Le temporaire vit DANS le dossier cible : `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.
|
||||
fd, tmp = tempfile.mkstemp(dir=str(path.parent), prefix=f".{path.name}.", suffix=".tmp")
|
||||
try:
|
||||
with os.fdopen(fd, "w", encoding=encoding) as fichier:
|
||||
yield fichier
|
||||
fichier.flush()
|
||||
# Sans `fsync`, le rename peut atteindre le disque AVANT le contenu : au
|
||||
# retour d'une coupure brutale, on aurait un fichier neuf et vide — le defaut
|
||||
# qu'on vient de fermer, deplace d'un cran.
|
||||
os.fsync(fichier.fileno())
|
||||
# Conserver les droits d'un fichier existant : `mkstemp` cree en 0600, et le plan
|
||||
# est lisible par le groupe sur les runners.
|
||||
if path.exists():
|
||||
os.chmod(tmp, path.stat().st_mode & 0o7777)
|
||||
os.replace(tmp, path)
|
||||
tmp = None
|
||||
finally:
|
||||
if tmp is not None and os.path.exists(tmp):
|
||||
os.unlink(tmp)
|
||||
|
||||
|
||||
def dependances_groupes(racine: Path | None = None) -> dict:
|
||||
"""Le registre des dependances causales entre groupes (docs/dependances-groupes.yml)."""
|
||||
f = (racine or RACINE_DEPOT) / "docs" / "dependances-groupes.yml"
|
||||
|
|
|
|||
|
|
@ -2634,9 +2634,10 @@ def preuve_documentation_outillage() -> tuple[bool, str]:
|
|||
PREUVES: list[dict] = [
|
||||
{"id": "P01", "titre": "Lint (ansible-lint)", "refs": ["AFF-006"],
|
||||
"cmds": [["ansible-lint", "-q"]]},
|
||||
{"id": "P02", "titre": "Tests unitaires (inventory_host)", "refs": [],
|
||||
{"id": "P02", "titre": "Tests unitaires (inventaire, raser, ecriture atomique)", "refs": [],
|
||||
"cmds": [[sys.executable, "scripts/tests/test_inventory_host.py"],
|
||||
[sys.executable, "scripts/tests/test_raser.py"]]},
|
||||
[sys.executable, "scripts/tests/test_raser.py"],
|
||||
[sys.executable, "scripts/tests/test_ecriture_atomique.py"]]},
|
||||
{"id": "P03", "titre": "Diff-vide du plan — TOUTES les instances", "refs": ["AFF-001", "AFF-004", "AFF-030", "AFF-031", "AFF-032"],
|
||||
"func": preuve_diff_vide_toutes_instances},
|
||||
{"id": "P04", "titre": "Groupes <-> playbooks homonymes", "refs": ["AFF-008"],
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import yaml
|
|||
|
||||
from inventory_gui import charger_yaml, liste_hotes
|
||||
from inventory_rules import (
|
||||
ecriture_atomique,
|
||||
charger_nomenclature,
|
||||
charger_serveurs,
|
||||
fonction_seq,
|
||||
|
|
@ -57,7 +58,7 @@ def ecrire(registre: dict) -> None:
|
|||
"# instance/plan/nomenclature.yml (jamais stockes ici).\n"
|
||||
"---\n"
|
||||
)
|
||||
with FICHIER.open("w", encoding="utf-8") as fichier:
|
||||
with ecriture_atomique(FICHIER) as fichier:
|
||||
fichier.write(entete)
|
||||
yaml.safe_dump({"serveurs": registre.get("serveurs", {}) or {}},
|
||||
fichier, default_flow_style=False, sort_keys=False, allow_unicode=True)
|
||||
|
|
|
|||
83
scripts/tests/test_ecriture_atomique.py
Normal file
83
scripts/tests/test_ecriture_atomique.py
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
"""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.")
|
||||
Loading…
Reference in a new issue