intrants du site : la table comparait cinq valeurs sur sept

Le controle rapportait CONFORME sur un locataire dont le plan d administration
pointait les bouts de WAN d un AUTRE site. nftables_admin_ssh et
passerelle_sortie n etaient compares par personne.

Quatorze machines debout, le runner insemine, et l exploitant bloque a la porte
sans qu aucune garde n ait rien eu a dire.

nftables_admin_ssh est une liste : la comparaison normalise, parce que l ordre
d une liste de sources ne porte aucun 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 22:52:19 -04:00
parent db1cb652b7
commit 8a9ac18462
3 changed files with 71 additions and 12 deletions

View file

@ -1,5 +1,38 @@
# CHANGELOG — Set-OPS
## 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
dont le plan d'administration pointait encore les bouts de WAN d'un AUTRE site.
`OU_LE_LOCATAIRE_LE_DIT` couvrait `dns_amorcage`, `artefacts_amorcage`,
`setops_depot_binaires`, `serveur_ops_forge_amont` et `client_backup_cible`. Pas
`nftables_admin_ssh`. Pas `passerelle_sortie`.
### Ce que le silence a coute
Quatorze machines materialisees, le runner insemine — et l'exploitant incapable d'entrer
sur son propre runner pour l'armer. `Connection timed out`, sans qu'aucune garde n'ait rien
eu a dire. La frontiere avait bien pose une regle d'administration, mais sur son interface
**WAN**, puisque c'est ce que le plan declarait. Bonne source, mauvaise porte.
### Ce qui change
Les deux entrees manquantes sont ajoutees. `nftables_admin_ssh` etant une **liste**, la
comparaison passe par une normalisation : l'ordre d'une liste de sources ne porte aucun
sens, et comparer `str(['a','b'])` a `str(['b','a'])` ferait crier une difference qui n'en
est pas une.
Les deux ecosystemes passent de cinq a six intrants compares — le septieme,
`passerelle_sortie`, n'est declare par aucun des deux : il se derive, et la garde le dit
plutot que de l'inventer.
### La regle, cinquieme occurrence
**Une liste qui en suit une autre prend du retard.** Le site expose ; la table compare. Deux
listes, et la seconde ne suivait pas. La garde s'ecrit EN MEME TEMPS que la seconde liste,
jamais quand on s'en sert.
## 2026-09-13 (8) — Le plancher traversait trois modules et se perdait au troisieme
Trouve en preparant la creation des quatorze machines de TechnoLibre, une commande avant

View file

@ -15,7 +15,7 @@
|---|---|---|---|---|
| P01 | Lint (ansible-lint) | AFF-006 | ✅ OK |  |
| P02 | Tests unitaires (inventaire, raser, ecritures du plan, rendu du GUI) | — | ✅ OK | OK |
| 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. |
| P03 | Diff-vide du plan — TOUTES les instances | AFF-001, AFF-004, AFF-030, AFF-031, AFF-032 | ✅ OK | 5 instance(s) verifiee(s) — instance-ci-1646753, 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 | |
| P06 | Validateurs de registres (serveurs/apps/bases/domaines) | AFF-003 | ✅ OK | Registre des domaines valide. |
@ -33,18 +33,18 @@
| P18 | Gabarit de voute complet | AFF-026 | ✅ OK | Gabarit de voute complet : 29 secret(s) exige(s), tous presents. (Voute reelle non lisible ici : verification sautee.) |
| P19 | Le GUI couvre le schema du plan | AFF-002, AFF-095 | ✅ OK | GUI : les 29 champ(s) des plans reels sont editables (2 plan(s) inspecte(s)), registres toleres : nomenclature. |
| P20 | Adressage 100% derive du seed (aucun stocke) | AFF-001, AFF-003 | ✅ OK | 2 nomenclature(s) : adressage 100% derive du seed index. |
| P21 | Federation : aucun index en collision | AFF-102 | ✅ OK | Federation coherente : 5 instance(s) federee(s), aucun index en collision. |
| P21 | Federation : aucun index en collision | AFF-102 | ✅ OK | Federation coherente : 6 instance(s) federee(s), aucun index en collision. |
| P22 | Plan de recette a jour (genere du wiki) | AFF-002 | ✅ OK | Plan de recette à jour (22 sections). |
| P23 | Underlay sans collision avec la plage tenant | AFF-103 | ✅ OK | Underlay conforme : 13 reseau(x), aucune collision avec la plage tenant. |
| P24 | Frontiere nord/sud : acces d'administration declare | AFF-104 | ✅ OK | note : serveur_powerdns declare un port `derive` que le plan du site ne resout pas — aucune regle emise. |
| P25 | Pare-feu Proxmox : est-ouest intra-tenant derive | AFF-107 | ✅ OK | CONFORME : pare-feu Proxmox, 3 tenant(s), 48 groupe(s), 85 regle(s). |
| P25 | Pare-feu Proxmox : est-ouest intra-tenant derive | AFF-107 | ✅ OK | CONFORME : pare-feu Proxmox, 4 tenant(s), 51 groupe(s), 90 regle(s). |
| P26 | Integrations universelles : aucun hote laisse de cote | AFF-108 | ✅ OK | 14 hote(s) x 6 integration(s) universelle(s) : aucune lacune, aucune recopie (0 exemption(s) derivee(s) du service rendu). |
| P27 | Propriete des intrants : hebergeur et tenant separes | AFF-109 | ✅ OK | 8 cle(s) de cluster chez l'hebergeur, aucune recopiee dans les group_vars du tenant. |
| P28 | Pools Proxmox : un par tenant, sans collision | AFF-110 | ✅ OK | CONFORME : 4 pool(s) Proxmox, 40 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 |
| 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. |
| P32 | Intrants exiges par les roles : tous fournis | — | ✅ OK | CONFORME : 38 exigence(s) de role, toutes satisfaites (146 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). |
| P34 | Chaque document declare son lecteur | — | ✅ OK | 45 document(s) declarent leur lecteur (39 genere(s) exempte(s)). |
| P35 | Toute application exigeant une base en a une au plan | — | ✅ OK | 5 application(s) exigeant une base l'ont toutes (4 entree(s) au registre). |
@ -54,7 +54,7 @@
| 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). |
| 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 |
| 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. |
| P45 | Pare-feu Proxmox : arme sur les VNet SDN, jamais ailleurs | — | ✅ OK | Le pare-feu Proxmox ne s'arme que sur un VNet SDN (4 cas evalues, dont un qui doit rendre VRAI). |
@ -62,12 +62,12 @@
| P47 | Zones inverses : couvrir l'occupe, et rien de plus | — | ✅ OK | Les zones inverses couvrent l'occupe et rien de plus (5 cas evalues, dont un site a quatre zones et un tenant a une). |
| P48 | La carte d'orientation designe ce qui existe, et compte juste | — | ✅ OK | La carte designe 89 chemin(s) qui existent, et ses 7 chiffres correspondent a la mesure. |
| P49 | Registre des flux : la matrice d'audit est a jour | — | ✅ OK | Le registre des flux reproduit exactement ce que les `meta/flux.yml` declarent (127 lignes). |
| P50 | Silences : un refus muet est declare, place en dernier, et motive | — | ✅ OK | 2 silence(s) declare(s), tous en sequence > 1 (la plus haute des 236 regles `pass`), tous non consignes et tous motives. |
| P50 | Silences : un refus muet est declare, place en dernier, et motive | — | ✅ OK | 2 silence(s) declare(s), tous en sequence > 1 (la plus haute des 240 regles `pass`), tous non consignes et tous motives. |
| P51 | Collections : toutes declarees, toutes epinglees | — | ✅ OK | 3 collection(s) et 2 bibliotheque(s) Python declarees et epinglees : ansible.posix==1.6.2, community.general==10.3.0, community.postgresql==3.10.2 |
| P52 | Materialiser n'exige pas d'entrer dans le tenant | — | ✅ OK | `creer-vm` confirme par l'agent invite (API des hyperviseurs, deja utilisee pour creer), sans exiger d'entrer dans le tenant. |
| P53 | L'interne refuse a voix haute, la bordure se tait | — | ✅ OK | L'interne parle, la bordure se tait — 14 ruleset(s) nftables refusent a voix haute ; pare-feu est-ouest en REJECT, source unique ; frontiere muette (actions : b |
| P54 | L'insemination ne reclame aucun secret du tenant | — | ✅ OK | 2 couche(s) d'insemination (serveur_debian, serveur_ops), 9 role(s) applique(s), aucun secret de tenant reclame. |
| P55 | La cle du SITE ne nait que sur le runner d'un tenant | — | ✅ OK | 14 hote(s) : la cle du SITE ne nait que sur 1 runner(s) de tenant, celle du tenant sur 0. |
| P55 | La cle du SITE ne nait que sur le runner d'un tenant | — | ✅ OK | 14 hote(s) : la cle du SITE ne nait que sur 1 runner(s) de tenant, celle du tenant sur 14. |
| P56 | Gabarit minimal, et rien de retire n'est perdu | — | ✅ OK | Gabarit minimal : 4 role(s), tous indispensables au premier demarrage ; 14 role(s) retire(s), tous repris par le socle ou le durcissement. |
| P57 | Comptes en prose : les chiffres du depot sur lui-meme | — | ✅ OK | Les comptes ecrits en prose correspondent a la mesure (75 preuves, 68 roles, 41 groupes). |
| P58 | Habilitations : chaque service dit a quel GROUPE, et par quoi | — | ✅ OK | 8 habilitation(s) declarees, toutes nommant un groupe, un mecanisme connu et une raison ; les `role-realm` sont projetees. |
@ -85,7 +85,7 @@
| P70 | Depot de binaires : il tient tout ce que les roles vont chercher | — | ✅ OK | 6 artefact(s) direct(s) tenus par le depot du site. |
| P71 | Pool du site : le genome ne nait pas chez un tenant | — | ✅ OK | `site-creer` nomme `--pool-site` ; le pool du genome ne peut plus etre celui d'un tenant. |
| P72 | Annuaire : aucun service ne se lie avec le compte du maitre | — | ✅ OK | 5 role(s) consultent l'annuaire, chacun avec SON compte de service ; seul `amorcage_acces` garde celui d'administration, et il provisionne au lieu de consommer. |
| P73 | Le locataire designe les services de son site REEL | — | ✅ OK | CONFORME : 5 intrant(s) du locataire concordent avec ce que le site expose. |
| P73 | Le locataire designe les services de son site REEL | — | ✅ OK | CONFORME : 6 intrant(s) du locataire concordent avec ce que le site expose (1 non declare(s), donc derive(s) ou non utilise(s)). |
| P74 | Gabarit dore : une seule declaration, au plan du site | — | ✅ OK | Gabarit declare une seule fois : VMID 9006 « modeleSetOPS-minimal », precedent 99998. |
| P75 | Les parametres de clonage traversent les trois maillons | — | ✅ OK | 14 parametre(s) de clonage, tous emis par l'inventaire. |

View file

@ -109,9 +109,35 @@ OU_LE_LOCATAIRE_LE_DIT = {
"setops_depot_binaires": ("all/10-intrants.yml", "setops_depot_binaires"),
"serveur_ops_forge_amont": ("serveur_ops.yml", "serveur_ops_forge_amont"),
"client_backup_cible": ("client_backup.yml", "client_backup_cible"),
# CES DEUX-LA MANQUAIENT, ET LE SILENCE A COUTE (2026-09-13). Le site en expose SEPT ;
# cette table n'en comparait que CINQ. Le controle rapportait donc « CONFORME » avec
# un aplomb complet, sur un locataire dont le plan d'administration pointait encore
# les bouts de WAN d'un AUTRE site.
#
# CE QUE CA A COUTE : quatorze machines materialisees, le runner insemine — et
# l'exploitant incapable d'entrer sur son propre runner pour l'armer. `Connection
# timed out`, sans qu'aucune garde n'ait rien eu a dire.
#
# Une liste qui en suit une autre prend du retard : la regle est connue ici, et c'est
# sa cinquieme occurrence. La garde s'ecrit EN MEME TEMPS que la seconde liste.
"nftables_admin_ssh": ("all/10-intrants.yml", "nftables_admin_ssh"),
"passerelle_sortie": ("all/10-intrants.yml", "passerelle_sortie"),
}
def _normaliser(v) -> str:
"""Une forme comparable, que la valeur soit une chaine ou une liste.
`nftables_admin_ssh` est une LISTE de CIDR ; les cinq intrants d'origine etaient tous
des chaines, et `str()` suffisait. Comparer `str(['a','b'])` a `str(['b','a'])` ferait
crier une difference qui n'en est pas une — l'ordre d'une liste de sources ne porte
aucun sens ici.
"""
if isinstance(v, (list, tuple)):
return " ".join(sorted(str(x).strip().rstrip("/") for x in v))
return str(v).strip().rstrip("/")
def _declare_par_le_locataire() -> dict[str, str]:
"""Lit, sans Ansible, ce que le locataire monte ecrit a propos de son site."""
# LA RESOLUTION D'INSTANCE EST PARTAGEE, PAS RECOPIEE (P41). La premiere version
@ -126,7 +152,7 @@ def _declare_par_le_locataire() -> dict[str, str]:
continue
d = yaml.safe_load(f.read_text(encoding="utf-8")) or {}
if variable in d:
vu[clef] = str(d[variable])
vu[clef] = d[variable]
return vu
@ -146,9 +172,9 @@ def verifier() -> int:
continue
# `setops_depot_binaires` se DERIVE legitimement d'`artefacts_amorcage` en Jinja :
# une expression n'est pas un retard, c'est le contraire.
if "{{" in vu[clef]:
if "{{" in str(vu[clef]):
continue
if vu[clef].rstrip("/") != str(attendu).rstrip("/"):
if _normaliser(vu[clef]) != _normaliser(attendu):
ecarts.append(f"{variable} : le locataire dit « {vu[clef]} », "
f"le site expose « {attendu} »")
if ecarts: