From 220e1e3e28e97d074503f67a505be961494cab3a Mon Sep 17 00:00:00 2001 From: Daniel Allaire Date: Sun, 13 Sep 2026 23:32:16 -0400 Subject: [PATCH] deployer-tout : le genome est-il a jour avant de poser quoi que ce soit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q --- CHANGELOG.md | 37 +++++++++ Makefile | 1 + docs/audit/preuve-2026-09-13.md | 4 +- scripts/verifier_genome_a_jour.py | 125 ++++++++++++++++++++++++++++++ 4 files changed, 165 insertions(+), 2 deletions(-) create mode 100755 scripts/verifier_genome_a_jour.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 7334280..cd9a4d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,42 @@ # 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 `site_intrants --verifier` rapportait **CONFORME** avec un aplomb complet sur un locataire diff --git a/Makefile b/Makefile index 3ecd1e2..73f416d 100644 --- a/Makefile +++ b/Makefile @@ -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.'; \ exit 2; \ fi; \ + python3 scripts/verifier_genome_a_jour.py || exit 2; \ python3 scripts/orchestrer.py verifier; \ python3 scripts/orchestrer.py ecrire; \ vault_chiffre="$$(grep -rlsIF '$$ANSIBLE_VAULT' $(dir $(INVENTAIRE_PRODUCTION))group_vars 2>/dev/null | head -1 || true)"; \ diff --git a/docs/audit/preuve-2026-09-13.md b/docs/audit/preuve-2026-09-13.md index a62ef16..01eac62 100644 --- a/docs/audit/preuve-2026-09-13.md +++ b/docs/audit/preuve-2026-09-13.md @@ -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. | | 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 | 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). | | 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)). | @@ -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 : 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 | | 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. | diff --git a/scripts/verifier_genome_a_jour.py b/scripts/verifier_genome_a_jour.py new file mode 100755 index 0000000..2b2e78b --- /dev/null +++ b/scripts/verifier_genome_a_jour.py @@ -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 pull --ff-only puis relancer", file=sys.stderr) + print(" FORCE=1 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())