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:
parent
ea67b75a15
commit
158c3b314a
3 changed files with 115 additions and 1 deletions
26
CHANGELOG.md
26
CHANGELOG.md
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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"]]},
|
||||
]
|
||||
|
|
|
|||
Loading…
Reference in a new issue