deployer-tout : le genome est-il a jour avant de poser quoi que ce soit

Un runner tire ce qu on pousse, il ne le recoit pas. Trois fois ce soir un
genome perime a menace de rebatir un etat depasse — et aucun des deux runners
n aurait echoue : un deploiement depuis un genome perime REUSSIT. Il applique
fidelement un etat qui n a plus cours, et se presente en vert.

En retard : refus. En avance ou diverge : note et on continue. Forge
injoignable : note et on continue — une forge en panne ne doit pas immobiliser
une exploitation, le silence serait la faute.

FORCE=1 passe outre. Eprouvee dans les trois sens.

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-13 23:32:16 -04:00
parent 8a9ac18462
commit 220e1e3e28
4 changed files with 165 additions and 2 deletions

View file

@ -1,5 +1,42 @@
# CHANGELOG — Set-OPS # CHANGELOG — Set-OPS
## 2026-09-13 (10) — Un runner TIRE ce qu'on pousse ; il ne le recoit pas
Trois fois dans une meme soiree, un genome perime a menace de rebatir un etat depasse :
- **le runner du SITE**, treize commits en retard, s'appretait a materialiser un locataire
avec son ancien plan — serveur de sauvegarde inutile, cache d'artefacts en trop, runner
sans pouvoir de configurer, et aucune machine avec son plancher de memoire ;
- **le runner du LOCATAIRE**, clone pendant l'insemination donc avant trois correctifs,
s'appretait a reposer un plan d'administration pointant le WAN d'un AUTRE site — et a
refermer derriere lui la porte que l'exploitant venait tout juste de rouvrir.
### Pourquoi c'est le pire mode de defaillance de cette famille
**Aucun des deux n'aurait echoue.** Un deploiement depuis un genome perime REUSSIT : il
applique fidelement un etat qui n'a plus cours. Il se presente en vert. Rien, dans la
sortie, ne distingue « la flotte converge vers ce que tu veux » de « la flotte converge
vers ce que tu voulais il y a trois heures ».
### La garde
`scripts/verifier_genome_a_jour.py`, en tete de `deployer-tout` :
| etat du depot | verdict |
|---|---|
| en RETARD | **refus** — deployer poserait un etat qu'on sait depasse |
| en AVANCE | note, on continue — un mainteneur qui travaille localement est normal |
| DIVERGE | note, on continue — c'est a l'humain de trancher, pas a une garde |
| forge injoignable | note, on continue — une forge en panne ne doit pas bloquer une exploitation |
Ce dernier point est delibere : **le silence serait la faute**, pas le passage. Une garde
qui immobilise la flotte quand la forge tousse serait pire que le defaut qu'elle surveille.
`FORCE=1` passe outre, pour le cas legitime ou l'on deploie sciemment un etat local.
Eprouvee dans les trois sens sur un clone recule de deux commits : elle mord, elle se
tait, et `FORCE=1` la leve.
## 2026-09-13 (9) — Le site expose sept intrants ; le controle n'en comparait que cinq ## 2026-09-13 (9) — Le site expose sept intrants ; le controle n'en comparait que cinq
`site_intrants --verifier` rapportait **CONFORME** avec un aplomb complet sur un locataire `site_intrants --verifier` rapportait **CONFORME** avec un aplomb complet sur un locataire

View file

@ -907,6 +907,7 @@ deployer-tout: _instance-requise ## Deploie TOUTE la flotte dans l'ordre des cou
printf '%s\n' 'Relancer avec CONFIRMER=true. Astuce: tester d abord en idempotent avec MODE_CHECK=1.'; \ printf '%s\n' 'Relancer avec CONFIRMER=true. Astuce: tester d abord en idempotent avec MODE_CHECK=1.'; \
exit 2; \ exit 2; \
fi; \ fi; \
python3 scripts/verifier_genome_a_jour.py || exit 2; \
python3 scripts/orchestrer.py verifier; \ python3 scripts/orchestrer.py verifier; \
python3 scripts/orchestrer.py ecrire; \ python3 scripts/orchestrer.py ecrire; \
vault_chiffre="$$(grep -rlsIF '$$ANSIBLE_VAULT' $(dir $(INVENTAIRE_PRODUCTION))group_vars 2>/dev/null | head -1 || true)"; \ vault_chiffre="$$(grep -rlsIF '$$ANSIBLE_VAULT' $(dir $(INVENTAIRE_PRODUCTION))group_vars 2>/dev/null | head -1 || true)"; \

View file

@ -43,7 +43,7 @@
| P28 | Pools Proxmox : un par tenant, sans collision | AFF-110 | ✅ OK | CONFORME : 5 pool(s) Proxmox, 44 VM placee(s), aucun nom ni VMID en collision. | | P28 | Pools Proxmox : un par tenant, sans collision | AFF-110 | ✅ OK | CONFORME : 5 pool(s) Proxmox, 44 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 | | 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. | | 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 | 64 scripts expliques et atteignables, 121 cibles make documentees, 68 roles avec README. | | P31 | Documentation : tout ce que le depot FAIT est nomme | — | ✅ OK | 65 scripts expliques et atteignables, 121 cibles make documentees, 68 roles avec README. |
| P32 | Intrants exiges par les roles : tous fournis | — | ✅ OK | CONFORME : 38 exigence(s) de role, toutes satisfaites (149 cle(s) declaree(s) par l'instance). | | P32 | Intrants exiges par les roles : tous fournis | — | ✅ OK | CONFORME : 38 exigence(s) de role, toutes satisfaites (149 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). | | 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 | 45 document(s) declarent leur lecteur (39 genere(s) exempte(s)). | | P34 | Chaque document declare son lecteur | — | ✅ OK | 45 document(s) declarent leur lecteur (39 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. | | 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. | | 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. | | 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 : 60 script(s) passent par `inventory_rules`, 3 exemption(s) nommee(s). | | P41 | Resolution d'instance : une seule, partagee | — | ✅ OK | Resolution unique : 61 script(s) passent par `inventory_rules`, 3 exemption(s) nommee(s). |
| P42 | L'edge porte les noms qu'il publie | — | ✅ OK | 5 edge(s) emettent un certificat portant les noms publies (instance-ci-1646753/production, OPS-Chezlepro-lab/principal, OPS-Chezlepro/principal, OPS-Technolibre | | P42 | L'edge porte les noms qu'il publie | — | ✅ OK | 5 edge(s) emettent un certificat portant les noms publies (instance-ci-1646753/production, OPS-Chezlepro-lab/principal, OPS-Chezlepro/principal, OPS-Technolibre |
| P43 | Frontiere : le devis voit les machines du site | — | ✅ OK | Devis de la frontiere : 7 machine(s) du plan retrouvees, 153 regle(s) du site. | | P43 | Frontiere : le devis voit les machines du site | — | ✅ OK | Devis de la frontiere : 7 machine(s) du plan retrouvees, 153 regle(s) du site. |
| P44 | Integrations : le serveur avant ses clients | — | ✅ OK | 5 integration(s) appliquent leur serveur avant leurs clients. | | P44 | Integrations : le serveur avant ses clients | — | ✅ OK | 5 integration(s) appliquent leur serveur avant leurs clients. |

125
scripts/verifier_genome_a_jour.py Executable file
View file

@ -0,0 +1,125 @@
#!/usr/bin/env python3
"""Les depots locaux sont-ils a jour sur leur forge, AVANT de deployer ?
UN RUNNER TIRE CE QU'ON POUSSE — IL NE LE RECOIT PAS (2026-09-13).
Trois fois dans une meme soiree, un runner perime a menace de rebatir un etat depasse :
- le runner du SITE, treize commits en retard, s'appretait a materialiser un locataire
avec son ancien plan — serveur de sauvegarde inutile, cache d'artefacts en trop,
runner sans pouvoir de configurer, et aucune machine avec son plancher de memoire ;
- le runner du LOCATAIRE, clone pendant l'insemination donc avant trois correctifs,
s'appretait a reposer un plan d'administration pointant le WAN d'un AUTRE site — et
a refermer derriere lui la porte que l'exploitant venait de rouvrir.
AUCUN DES DEUX N'AURAIT ECHOUE. Un deploiement depuis un genome perime REUSSIT : il
applique fidelement un etat qui n'a plus cours. C'est le mode de defaillance le plus
couteux de cette famille, parce qu'il se presente en vert.
CE QUE CETTE GARDE FAIT, ET CE QU'ELLE NE FAIT PAS :
en RETARD -> REFUS. Deployer poserait un etat qu'on sait depasse.
en AVANCE -> note, et on continue. Un mainteneur qui travaille localement est normal.
DIVERGE -> note, et on continue. C'est a l'humain de trancher, pas a une garde.
injoignable -> note, et on continue. Une forge en panne ne doit pas bloquer une
exploitation ; on le DIT, c'est tout. Le silence serait la faute.
`FORCE=1` passe outre — pour le cas legitime ou l'on deploie sciemment un etat local.
"""
from __future__ import annotations
import os
import subprocess
import sys
from pathlib import Path
RACINE = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(RACINE / "scripts"))
DELAI_FETCH = 20 # s — une forge lente ne doit pas immobiliser un deploiement
def _git(depot: Path, *args: str, delai: int | None = None) -> tuple[int, str]:
try:
p = subprocess.run(["git", "-C", str(depot), *args],
capture_output=True, text=True, timeout=delai)
return p.returncode, (p.stdout or p.stderr).strip()
except subprocess.TimeoutExpired:
return 124, "delai depasse"
except Exception as e: # git absent, droits…
return 1, f"{e.__class__.__name__}: {e}"
def depots_a_verifier() -> list[tuple[str, Path]]:
"""Le moteur, et l'ecosysteme monte. Les deux que le deploiement LIT."""
out: list[tuple[str, Path]] = [("moteur", RACINE)]
try:
from inventory_rules import instance_courante
inst = instance_courante()
if inst and Path(inst).resolve() != RACINE:
out.append(("instance", Path(inst).resolve()))
except Exception:
pass
return out
def etat(depot: Path) -> tuple[str, str]:
"""(verdict, detail) — verdict dans {a-jour, retard, avance, diverge, inconnu}."""
if not (depot / ".git").exists():
return "inconnu", "pas un depot git"
rc, _ = _git(depot, "fetch", "--quiet", delai=DELAI_FETCH)
if rc != 0:
return "inconnu", "forge injoignable"
rc, amont = _git(depot, "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}")
if rc != 0:
return "inconnu", "aucune branche de suivi"
rc, compte = _git(depot, "rev-list", "--left-right", "--count", f"HEAD...{amont}")
if rc != 0:
return "inconnu", compte
devant, derriere = (compte.split() + ["0", "0"])[:2]
devant, derriere = int(devant), int(derriere)
if derriere and devant:
return "diverge", f"{devant} devant, {derriere} derriere ({amont})"
if derriere:
return "retard", f"{derriere} commit(s) derriere {amont}"
if devant:
return "avance", f"{devant} commit(s) devant {amont}"
return "a-jour", amont
def verifier() -> int:
retards, notes = [], []
for role, depot in depots_a_verifier():
verdict, detail = etat(depot)
ligne = f" {role:<9} {depot.name:<20} {detail}"
if verdict == "retard":
retards.append(ligne)
elif verdict != "a-jour":
notes.append(f"{ligne} [{verdict}]")
for n in notes:
print("note :" + n, file=sys.stderr)
if not retards:
print("OK : genome a jour.")
return 0
print("Refus: le genome local est EN RETARD sur sa forge.", file=sys.stderr)
for r in retards:
print(r, file=sys.stderr)
print("", file=sys.stderr)
print("Un deploiement depuis un genome perime REUSSIT — il applique fidelement",
file=sys.stderr)
print("un etat qui n'a plus cours, et se presente en vert.", file=sys.stderr)
print("", file=sys.stderr)
print(" git -C <depot> pull --ff-only puis relancer", file=sys.stderr)
print(" FORCE=1 <commande> pour deployer l'etat local sciemment",
file=sys.stderr)
return 2
if __name__ == "__main__":
if os.environ.get("FORCE") in ("1", "true", "yes"):
print("note : FORCE=1 — controle du genome passe outre.", file=sys.stderr)
raise SystemExit(0)
raise SystemExit(verifier())