preuve : P43 — la frontiere voit-elle les machines du site ?

Couvre ce qui a failli coûter 36 objets ce soir : le devis de la frontiere avait cesse de
voir le site et proposait de retirer tous ses alias et toutes ses regles. Harnais vert,
lint vert — seule la lecture manuelle du plan avant application l'a attrape.

La premiere version etait inutile, et c'est instructif : elle lisait le plan par
`devis_opnsense._machines_du_plan_site()`, la fonction meme dont la panne etait a
detecter. Eprouvee sur la regression reelle, elle SE TAISAIT — les deux voyaient le vide,
et elle concluait « rien a prouver ». Une preuve qui partage la source de ce qu'elle
verifie ne verifie rien.

La version retenue lit plan/serveurs.yml et plan/applications.yml DIRECTEMENT et confronte
au devis produit. Eprouvee sur la regression reelle : elle refuse et nomme la cause.

43 preuves vertes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Daniel Allaire 2026-08-25 14:11:39 -04:00
parent ea67b75a15
commit 158c3b314a
3 changed files with 115 additions and 1 deletions

View file

@ -1,5 +1,31 @@
# CHANGELOG — Set-OPS
## 2026-08-25 — P43 : la frontière voit-elle les machines du site ?
**43 preuves.** Celle-ci couvre ce qui a failli coûter 36 objets ce soir : le devis de la
frontière avait cessé de voir le site, et proposait de retirer tous ses alias et toutes
ses règles. La frontière aurait laissé tomber la forge du génome, le cache racine et le
runner du site, d'un seul `CONFIRMER=true`.
Rien ne l'avait signalé — harnais vert, `ansible-lint` vert. Seule la lecture manuelle du
plan avant application l'a attrapé.
### La première version était inutile, et c'est instructif
Elle lisait le plan par `devis_opnsense._machines_du_plan_site()` — **la fonction même
dont la panne était à détecter**. Éprouvée sur la régression réelle, elle ne criait pas :
elle **se taisait**. Les deux voyaient le vide, et la preuve concluait « rien à prouver ».
> Une preuve qui partage la source de ce qu'elle vérifie ne vérifie rien.
C'est la même erreur d'instrument qui a coûté quatre faux diagnostics cette session : le
VPN pris pour la frontière, `ping` pour du TCP, l'`overview` d'OPNsense pour ses
assignations. Ici elle était logée dans la preuve elle-même.
La version retenue lit `plan/serveurs.yml` et `plan/applications.yml` **directement**, et
confronte au devis produit. Éprouvée sur la régression réelle : elle refuse, et nomme la
cause — *« `SETOPS_SITE` est absent ou vide, alors que le plan déclare 5 machines »*.
## 2026-08-25 — Le site résout chez lui, et le socle cesse de le défaire
**Mise au point de l'exploitant** : la frontière OPNsense n'a pas de service DNS actif et

View file

@ -7,7 +7,7 @@
> [`docs/audit/affirmations.md`](affirmations.md).
- **Instance** : `instance` — inventaire `instance/inventories/production/hosts.yml`
- **Verdict** : ✅ CONFORME (42 OK · 0 echec · 0 saute)
- **Verdict** : ✅ CONFORME (43 OK · 0 echec · 0 saute)
## Preuves
@ -55,6 +55,7 @@
| 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 : 45 script(s) passent par `inventory_rules`, 3 exemption(s) nommee(s). |
| P42 | L'edge porte les noms qu'il publie | — | ✅ OK | 4 edge(s) emettent un certificat portant les noms publies (OPS-Chezlepro-lab/principal, OPS-Chezlepro/principal, OPS-Technolibre/principal, OPS-Patient0/product |
| P43 | Frontiere : le devis voit les machines du site | — | ✅ OK | Devis de la frontiere : 5 machine(s) du plan retrouvees, 21 regle(s) du site. |
## Couverture des affirmations ✅ du registre

View file

@ -928,6 +928,91 @@ def preuve_parente_inscrite() -> tuple[bool, str]:
return r.returncode == 0, (sortie[-1] if sortie else "")
def preuve_devis_frontiere_du_site() -> tuple[bool, str]:
"""Le devis de la frontiere VOIT les machines du site — lues independamment.
POURQUOI CETTE PREUVE EXISTE (2026-08-25). En deplacant le plan du site hors de
`underlay.yml`, `underlay.machines()` s'est vide sans que `devis_opnsense` soit
rebranche. Le devis a cesse de voir le site et a propose de RETIRER 36 objets — tous
ses alias et toutes ses regles. La frontiere aurait laisse tomber la forge du genome,
le cache racine et le runner du site, d'un seul `CONFIRMER=true`.
Rien ne l'a signale : le harnais est reste vert, `ansible-lint` aussi. Seule la
lecture MANUELLE du plan avant application l'a attrape.
ELLE LIT LE PLAN ELLE-MEME, ET C'EST TOUT SON INTERET. Un premier jet passait par
`devis_opnsense._machines_du_plan_site()` — la fonction meme dont la panne etait a
detecter. Eprouve sur la regression reelle, il SE TAISAIT : les deux voyaient le vide,
et la preuve concluait « rien a prouver ». Une preuve qui partage la source de ce
qu'elle verifie ne verifie rien.
Elle se tait quand il n'y a pas de plan de site — un depot sans underlay monte n'a
rien a prouver ici — mais jamais quand le plan existe et que le devis l'ignore.
"""
import ipaddress
sys.path.insert(0, str(RACINE / "scripts"))
import underlay as underlay_mod
import devis_opnsense as D
import devis_reseau
# --- LE TEMOIN : le plan, lu directement, sans passer par le devis ---------
chemin = underlay_mod.chemin()
plan = (chemin.resolve().parent / "plan") if chemin else None
f_srv = (plan / "serveurs.yml") if plan else None
if not f_srv or not f_srv.is_file():
return True, "Aucun plan de site (pas de `plan/serveurs.yml`) : rien a exiger."
serveurs = (yaml.safe_load(f_srv.read_text(encoding="utf-8")) or {}).get("serveurs") or {}
attendus = {n: s for n, s in serveurs.items()
if str(s.get("etat", "actif")) == "actif"}
if not attendus:
return True, "Plan de site sans machine active : rien a exiger."
f_app = plan / "applications.yml"
apps = ((yaml.safe_load(f_app.read_text(encoding="utf-8")) or {}).get("applications") or {}
) if f_app.is_file() else {}
roles_par_hote: dict[str, list[str]] = {}
for app in apps.values():
if app.get("hote") and app.get("groupe"):
roles_par_hote.setdefault(str(app["hote"]), []).append(str(app["groupe"]))
# --- CE QUE LE DEVIS PRODUIT, au meme perimetre que `make frontiere-plan` --
devis = D.construire(devis_reseau.decouvrir_du_site())
alias = devis.get("alias") or {}
regles = devis.get("regles") or []
manques: list[str] = []
reseaux = [str(x) for x in ((alias.get("SETOPS_SITE") or {}).get("contenu") or [])]
if not reseaux:
manques.append(f"`SETOPS_SITE` est absent ou vide, alors que le plan declare "
f"{len(attendus)} machine(s) : le devis ne voit pas le site")
else:
for nom, srv in attendus.items():
if not any(ipaddress.ip_address(str(srv["ip"])) in
ipaddress.ip_network(r, strict=False) for r in reseaux):
manques.append(f"{nom} ({srv['ip']}) n'est dans aucun sous-reseau de "
f"`SETOPS_SITE`")
for hote, groupes in roles_par_hote.items():
if hote not in attendus:
continue
for g in groupes:
attendu = D.nom_alias(g, "SITE")
if attendu not in alias:
manques.append(f"{hote} porte `{g}` au plan, mais l'alias `{attendu}` "
f"n'est pas au devis")
n_site = sum(1 for r in regles if str(r.get("tenant")) == "SITE")
if not n_site:
manques.append("aucune regle du devis ne porte sur le site : la frontiere ne "
"laisserait rien passer vers ses machines")
if manques:
return False, ("Le devis de la frontiere ne voit pas le site :\n - "
+ "\n - ".join(manques))
return True, (f"Devis de la frontiere : {len(attendus)} machine(s) du plan retrouvees, "
f"{n_site} regle(s) du site.")
def preuve_glossaire_enseigne() -> tuple[bool, str]:
"""Tout mot que le depot emploie devant l'exploitant est explique au glossaire.
@ -1266,6 +1351,8 @@ PREUVES: list[dict] = [
"func": preuve_resolution_unique},
{"id": "P42", "titre": "L'edge porte les noms qu'il publie", "refs": [],
"func": preuve_edge_porte_ses_noms},
{"id": "P43", "titre": "Frontiere : le devis voit les machines du site", "refs": [],
"func": preuve_devis_frontiere_du_site},
{"id": "P33", "titre": "Aucune collision de port entre roles co-localises", "refs": [],
"cmds": [[sys.executable, "scripts/verifier_ports.py"]]},
]