intrants du site : ce qu un locataire doit savoir pour l habiter, derive

Un locataire ECRIT les adresses des services de son site — resolveur, cache, forge,
depot de sauvegarde, plan d administration, sortie. Une copie se perime, et deux
l avaient fait en deux jours avec la meme forme : `serveur_ops_forge_amont` visait
10.0.33.11 quand la forge sert en 10.37.33.11, et `ac-racine-site.crt` portait la
racine d avant la reconstruction du site. Rien ne les relisait.

`make site-intrants` lit le plan du site et rend le contrat — sept valeurs, toutes
derivees. `make site-intrants-verifier` les confronte a ce que le locataire declare,
et P73 en fait une preuve. Elle a trouve le defaut de la forge des sa premiere
execution.

Le contrat se DERIVE du plan du site, pas d une liste tenue a part : ajouter un
service prete au site l ajoute au contrat, sans qu on ait a y penser.

P69 RESTREINTE AU COUPLE MONTE. Elle balayait tous les depots OPS-* et les comparait
au site monte. Elle avait raison tant qu un seul site existait : une adresse en
10.x.3z ne pouvait designer que lui. Deux sites decoupent leurs zones de la meme
facon — c est le but, un locataire doit pouvoir habiter l un ou l autre sans se
renumeroter. Le troisieme octet a cesse de distinguer « mon site » d « un autre
site » : 10.31.34.11, juste pour un locataire de TechnoLibre, etait declare faux
parce que Chezlepro etait monte.

Un locataire n appartient a aucun site — il en habite un, choisi par le symlink au
deploiement. La seule paire jugeable est celle qui est montee. Meme portee que P73.

Une marche payee : la premiere version de site_intrants recopiait la resolution
d instance au lieu de la partager. P41 a mordu — neuf modules avaient deja porte
chacun leur copie, et cinq defauts en etaient sortis en cinq jours.

make prouver : 72 OK, 0 echec, 1 saute. ansible-lint : 0 failure.

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 16:18:12 -04:00
parent b4e3520561
commit 025776064a
7 changed files with 308 additions and 8 deletions

View file

@ -188,7 +188,7 @@ Si `ansible-lint` n’est pas disponible, le signaler clairement. Ne pas invente
## Écrire, puis relire (D-68)
`--syntax-check` et `ansible-lint` prouvent que le dépôt est cohérent **avec lui-même**.
C'est aussi ce que font les 72 preuves de `make prouver` : elles lisent le dépôt, sans le
C'est aussi ce que font les 73 preuves de `make prouver` : elles lisent le dépôt, sans le
moindre appel réseau. **Aucune ne demande au système déployé s'il ressemble à ce que le
dépôt annonce.**

View file

@ -1,5 +1,57 @@
# CHANGELOG — Set-OPS
## 2026-09-13 (3) — Un site expose ce dont ses locataires ont besoin pour l'habiter
### Le constat
Un locataire ECRIT dans ses intrants les adresses des services de son site : ou resoudre,
ou prendre ses paquets, ou cloner le genome, ou deposer son etat. Une copie se perime, et
deux l'avaient fait en deux jours, avec exactement la meme forme :
- `serveur_ops_forge_amont: https://10.0.33.11` alors que la forge sert en `10.37.33.11`
depuis que le site a pris son propre index. Le commentaire juste au-dessus expliquait
encore pourquoi l'ancienne adresse figurait dans les SAN du certificat : le raisonnement
etait intact, la valeur non.
- `ac-racine-site.crt` versionne portait la racine du site d'AVANT sa reconstruction.
Aucune des deux n'etait relue par quoi que ce soit.
### `make site-intrants`
Le site lit son propre plan et rend le contrat — sept valeurs, toutes DERIVEES :
```
dns_amorcage · artefacts_amorcage · setops_depot_binaires · serveur_ops_forge_amont
client_backup_cible · nftables_admin_ssh · passerelle_sortie
```
`make site-intrants-verifier` les confronte a ce que le locataire monte declare, et **P73**
en fait une preuve. Elle a trouve le defaut de la forge des sa premiere execution.
Le contrat se DERIVE du plan du site, pas d'une liste tenue a part : ajouter un service
prete au site l'ajoute au contrat, sans qu'on ait a y penser.
### P69 restreinte au couple monte
Elle balayait TOUS les depots `OPS-*` et les comparait au site MONTE. Elle avait raison
tant qu'un seul site existait : une adresse « en 10.x.3z » ne pouvait designer que lui.
Deux sites decoupent leurs zones de la meme facon — c'est le but, un locataire doit pouvoir
habiter l'un ou l'autre sans se renumeroter. Le troisieme octet a donc cesse de distinguer
« mon site » d'« un autre site » : `10.31.34.11`, parfaitement juste pour un locataire de
TechnoLibre, etait declare faux parce que Chezlepro etait monte.
**Un locataire n'appartient a aucun site — il en habite un**, choisi par le symlink au
moment du deploiement. La seule paire qu'on puisse juger est donc celle qui est montee.
### Une marche payee en chemin
La premiere version de `site_intrants` ecrivait `RACINE / "instance" / "inventories" /
"principal"`. **P41 a mordu** : neuf modules avaient deja porte chacun leur copie de cette
resolution, et cinq defauts en etaient sortis en cinq jours. La preuve a attrape la
dixieme avant qu'elle serve.
## 2026-09-13 (2) — L'annuaire cesse de tout ouvrir avec la meme cle
### Ce que la mesure a montre

View file

@ -776,6 +776,13 @@ devis-proxmox-fw: ansible-runtime ## Devis pare-feu Proxmox (est-ouest intra-ten
devis-proxmox-fw-verifier: ## Verifie le devis du pare-feu est-ouest Proxmox (aucune ecriture)
python3 scripts/devis_proxmox_fw.py --verifier
.PHONY: site-intrants site-intrants-verifier
site-intrants: ## Les intrants que le SITE monte expose a ses locataires (derives)
python3 scripts/site_intrants.py $(if $(JSON),--json,)
site-intrants-verifier: ## Le locataire monte suit-il encore son site ? (aucune ecriture)
python3 scripts/site_intrants.py --verifier
.PHONY: devis-proxmox-pools devis-proxmox-pools-verifier
devis-proxmox-pools: ansible-runtime ## Devis des pools Proxmox (un par tenant), derive du plan
python3 scripts/devis_proxmox_pools.py $(if $(JSON),--json,)

View file

@ -7,7 +7,7 @@
> [`docs/audit/affirmations.md`](affirmations.md).
- **Instance** : `/home/danallaire/Espace Chezlepro/DépôtsSurForge/Set-OPS-public/instance` — inventaire `/home/danallaire/Espace Chezlepro/DépôtsSurForge/Set-OPS-public/instance/inventories/principal/hosts.yml`
- **Verdict** : ✅ CONFORME (71 OK · 0 echec · 1 saute)
- **Verdict** : ✅ CONFORME (72 OK · 0 echec · 1 saute)
## Preuves
@ -43,7 +43,7 @@
| P28 | Pools Proxmox : un par tenant, sans collision | AFF-110 | ✅ OK | CONFORME : 4 pool(s) Proxmox, 41 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 | 62 scripts expliques et atteignables, 119 cibles make documentees, 68 roles avec README. |
| P31 | Documentation : tout ce que le depot FAIT est nomme | — | ✅ OK | 63 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 (139 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 | 44 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 : 58 script(s) passent par `inventory_rules`, 3 exemption(s) nommee(s). |
| P41 | Resolution d'instance : une seule, partagee | — | ✅ OK | Resolution unique : 59 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 : 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. |
@ -69,7 +69,7 @@
| 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 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 (72 preuves, 68 roles, 41 groupes). |
| P57 | Comptes en prose : les chiffres du depot sur lui-meme | — | ✅ OK | Les comptes ecrits en prose correspondent a la mesure (73 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. |
| P59 | Enumerations annoncees : le nombre correspond a ce qui suit | — | ✅ OK | 2 enumeration(s) annoncee(s) correspondent a ce qu'elles annoncent (formes non ambigues seulement). |
| P60 | Wiki publie : la forge sert ce que le depot dit | AFF-002 | ✅ OK | Le wiki publie correspond au depot : `wiki/` n'a pas bouge depuis `904ece3` (publie le 2026-09-12). |
@ -85,6 +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. |
## Couverture des affirmations ✅ du registre

View file

@ -30,7 +30,7 @@ make placement-plan # chaque VM est-elle là où le plan la met
## Le trou qu'il comble
`scripts/prouver.py` porte 72 preuves (dont une conditionnelle, sautée sans la clé de la voûte). Elles sont toutes **statiques** : elles lisent le
`scripts/prouver.py` porte 73 preuves (dont une conditionnelle, sautée sans la clé de la voûte). Elles sont toutes **statiques** : elles lisent le
dépôt. Zéro appel réseau, zéro SSH, zéro `ansible`. Elles établissent que le dépôt est
cohérent **avec lui-même** — que les handlers existent, que les intrants ont un
propriétaire, que rien n'est codé en dur.

View file

@ -3313,6 +3313,35 @@ def preuve_pool_du_site_nest_pas_celui_du_tenant() -> tuple[bool, str]:
def preuve_locataire_suit_son_site() -> tuple[bool, str]:
"""Le locataire monte designe-t-il encore les services de son site REEL ?
UN SITE EST UNE INTERFACE. Un locataire qui l'habite a besoin d'un petit nombre de
valeurs : ou resoudre, ou prendre ses paquets et ses binaires, ou cloner le genome, ou
deposer son etat. Il les ECRIT dans ses intrants — et une copie se perime.
DEUX FOIS EN DEUX JOURS, avec la meme forme :
- `serveur_ops_forge_amont: https://10.0.33.11` alors que la forge sert en
10.37.33.11 depuis que le site a pris son propre index. Le commentaire au-dessus
expliquait encore pourquoi l'ancienne adresse etait dans les SAN du certificat :
le raisonnement intact, la valeur perimee.
- une carte de site dans le depot du LOCATAIRE, decrivant un index 23 quand le vrai
disait 31.
P69 couvrait deja `dns_amorcage` et `artefacts_amorcage`. Celle-ci couvre le reste du
contrat, et se derive du site plutot que d'une liste tenue a part : ajouter un service
prete au site l'ajoute ici, sans qu'on ait a y penser.
"""
import subprocess
r = subprocess.run([sys.executable, str(RACINE / "scripts" / "site_intrants.py"),
"--verifier"], capture_output=True, text=True, cwd=RACINE)
sortie = (r.stdout + r.stderr).strip()
if r.returncode == 0:
return True, sortie.splitlines()[0] if sortie else "Aucun site monte."
return False, " ".join(l.strip(" -") for l in sortie.splitlines() if l.strip())
def preuve_annuaire_sans_compte_maitre() -> tuple[bool, str]:
"""Aucun service ne se lie-t-il a l'annuaire avec son compte d'administration ?
@ -3448,8 +3477,24 @@ def preuve_amorcage_suit_le_site() -> tuple[bool, str]:
# Les troisiemes octets des zones du site — la signature d'une adresse « du site ».
octets_site = {a.split(".")[2] for v in attendus.values() for a in v if a.count(".") == 3}
# LE COUPLE MONTE, ET LUI SEUL (2026-09-13).
#
# Cette preuve balayait TOUS les depots `OPS-*` et les comparait au site MONTE. Elle
# avait raison tant qu'un seul site existait : une adresse « en 10.x.3z » ne pouvait
# designer que lui.
#
# Deux sites decoupent leurs zones de la meme facon — c'est le but, un locataire doit
# pouvoir habiter l'un ou l'autre sans se renumeroter. Le troisieme octet a donc cesse
# de distinguer « mon site » d'« un autre site » : `10.31.34.11`, parfaitement juste
# pour un locataire de TechnoLibre, etait declare faux parce que Chezlepro etait monte.
#
# UN LOCATAIRE N'APPARTIENT A AUCUN SITE — il en habite un, choisi par le symlink au
# moment du deploiement. La seule paire qu'on puisse juger est donc celle qui est
# montee. Meme portee que P73, et pour la meme raison.
from inventory_rules import dossier_inventaire, instance_courante
fautes, verifies = [], 0
for intrants in sorted((RACINE.parent).glob("OPS-*/inventories/*/group_vars/all/10-intrants.yml")):
_f = dossier_inventaire() / "group_vars" / "all" / "10-intrants.yml"
for intrants in ([_f] if _f.is_file() else []):
try:
d = yaml.safe_load(intrants.read_text(encoding="utf-8")) or {}
except Exception:
@ -3463,7 +3508,7 @@ def preuve_amorcage_suit_le_site() -> tuple[bool, str]:
continue # ne pretend pas designer le site : pas notre affaire
verifies += 1
if adr not in bons:
fautes.append(f"{intrants.parents[3].name} : `{cle}` vise {adr}, "
fautes.append(f"{instance_courante().resolve().name} : `{cle}` vise {adr}, "
f"le site porte {', '.join(bons)}")
if not verifies:
return True, "Aucun tenant ne s'amorce sur une adresse du site."
@ -3638,6 +3683,8 @@ PREUVES: list[dict] = [
"refs": [], "func": preuve_pool_du_site_nest_pas_celui_du_tenant},
{"id": "P72", "titre": "Annuaire : aucun service ne se lie avec le compte du maitre",
"refs": [], "func": preuve_annuaire_sans_compte_maitre},
{"id": "P73", "titre": "Le locataire designe les services de son site REEL",
"refs": [], "func": preuve_locataire_suit_son_site},
]

193
scripts/site_intrants.py Normal file
View file

@ -0,0 +1,193 @@
#!/usr/bin/env python3
"""Les intrants qu'un SITE expose a ses locataires — derives, jamais ecrits.
UN SITE EST UNE INTERFACE, PAS UN DECOR. Un locataire qui veut l'habiter a besoin d'un
petit nombre de valeurs : ou resoudre, ou prendre ses paquets, ou cloner le genome, ou
deposer son etat, quel plan d'administration laisser entrer. Aujourd'hui il les ECRIT A
LA MAIN dans ses intrants, et le depot s'en remet a une garde pour verifier qu'elles
n'ont pas vieilli.
CE QUE CA COUTE, MESURE DEUX FOIS EN DEUX JOURS :
- `serveur_ops_forge_amont: https://10.0.33.11` chez Chezlepro — la forge avait demenage
en 10.37.33.11 avec le renumerotage du site. Le commentaire juste au-dessus expliquait
encore pourquoi l'ancienne adresse figurait dans les SAN du certificat : le raisonnement
etait intact, la valeur non.
- `OPS-Technolibre/underlay.yml` decrivait un site d'index 23 quand le vrai disait 31.
Les deux avaient la meme forme : une copie qui a cesse de designer ce qu'elle nommait, et
que rien ne relisait. Une valeur DERIVEE ne peut pas avoir ce defaut.
CE SCRIPT NE MODIFIE RIEN. Il lit le site monte (`underlay.yml` et son `plan/`) et rend le
contrat. `--verifier` le confronte a ce que le locataire monte declare, et c'est la que le
retard se voit.
Usage :
python3 scripts/site_intrants.py # le contrat, lisible
python3 scripts/site_intrants.py --json # le meme, pour l'API et les gabarits
python3 scripts/site_intrants.py --verifier # garde : le locataire suit-il son site ?
"""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
RACINE = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(RACINE / "scripts"))
import underlay as underlay_mod # noqa: E402
from inventory_rules import dossier_inventaire # noqa: E402
import yaml # noqa: E402
PORT_CACHE = 3142
PREFIXE_BINAIRES = "setops-binaires"
def _une(groupe: str) -> str:
"""L'adresse du site qui porte ce groupe, ou '' s'il n'y en a pas.
PLUSIEURS ADRESSES = AUCUNE REPONSE. Un service porte par deux machines demande un
choix (lequel amorce ?) que ce script n'a pas a faire silencieusement.
"""
a = underlay_mod.adresses_site_portant(groupe)
return a[0] if len(a) == 1 else ""
def _nom_expose(groupe: str, domaine: str) -> str:
"""Le nom de SERVICE que le site publie pour ce groupe, si son plan en declare un."""
apps = underlay_mod.lire_plan_site("applications.yml").get("applications") or {}
for a in apps.values():
if a.get("groupe") == groupe:
for e in (a.get("expose") or []):
return str(e)
hote = next((m["nom"] for m in underlay_mod.machines_site()
if groupe in (underlay_mod.services_site().get(m["nom"]) or [])), "")
return f"{hote}.{domaine}" if hote and domaine else ""
def contrat() -> dict:
u = underlay_mod.charger()
if not u:
return {}
intrants = underlay_mod.lire_plan_site("10-intrants.yml")
domaine = str(intrants.get("domaine_interne") or "")
gestion = next((r.get("sous_reseau") for r in underlay_mod.reseaux(u)
if r.get("nom") == "management"), "")
transit = underlay_mod.reseau_transit(u) or {}
cache = _une("serveur_artefacts")
forge = _une("serveur_forgejo")
return {
# QUI EST CE SITE
"domaine_site": domaine,
"index_site": underlay_mod.index_site(u),
# OU RESOUDRE, AVANT QUE LE LOCATAIRE AIT SON PROPRE RESOLVEUR
"dns_amorcage": _une("serveur_resolveur"),
# OU PRENDRE SES PAQUETS, ET SES BINAIRES DE PUBLICATION
"artefacts_amorcage": f"{cache}:{PORT_CACHE}" if cache else "",
"setops_depot_binaires": (f"http://{cache}:{PORT_CACHE}/{PREFIXE_BINAIRES}"
if cache else ""),
# OU CLONER LE GENOME, ET AVEC QUELLE RACINE LUI FAIRE CONFIANCE
"serveur_ops_forge_amont": f"https://{forge}" if forge else "",
"forge_amont_ac": str((underlay_mod.chemin() or Path()).resolve().parent
/ "ac-racine-site.crt"),
# OU DEPOSER SON ETAT — par NOM : le depot sert plusieurs locataires et peut bouger
"client_backup_cible": _nom_expose("serveur_backup_site", domaine),
# QUI A LE DROIT D'ENTRER EN SSH
"nftables_admin_ssh": [gestion] if gestion else [],
# PAR OU SORTIR, ET A QUI DEMANDER L'HEURE
"passerelle_sortie": str(transit.get("passerelle_sortie") or ""),
}
# Ce que le locataire monte declare, et sous quel nom, pour chaque clef du contrat.
# Vide = le locataire ne declare rien de tel, et c'est legitime (il derive deja).
OU_LE_LOCATAIRE_LE_DIT = {
"dns_amorcage": ("all/10-intrants.yml", "dns_amorcage"),
"artefacts_amorcage": ("all/10-intrants.yml", "artefacts_amorcage"),
"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"),
}
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
# ecrivait `RACINE / "instance" / "inventories" / "principal"` — le symlink et le nom
# du dossier en dur. Neuf modules avaient deja porte chacun leur copie, et cinq
# defauts en etaient sortis en cinq jours. La preuve a mordu sur la dixieme.
base = dossier_inventaire() / "group_vars"
vu: dict[str, str] = {}
for clef, (fichier, variable) in OU_LE_LOCATAIRE_LE_DIT.items():
f = base / fichier
if not f.is_file():
continue
d = yaml.safe_load(f.read_text(encoding="utf-8")) or {}
if variable in d:
vu[clef] = str(d[variable])
return vu
def verifier() -> int:
c = contrat()
if not c:
print("Aucun site monte : rien a exposer.")
return 0
vu = _declare_par_le_locataire()
ecarts, absents = [], []
for clef, attendu in c.items():
if clef not in OU_LE_LOCATAIRE_LE_DIT or not attendu:
continue
fichier, variable = OU_LE_LOCATAIRE_LE_DIT[clef]
if clef not in vu:
absents.append(f"{variable} ({fichier})")
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]:
continue
if vu[clef].rstrip("/") != str(attendu).rstrip("/"):
ecarts.append(f"{variable} : le locataire dit « {vu[clef]} », "
f"le site expose « {attendu} »")
if ecarts:
print("NON CONFORME : le locataire ne suit plus son site.")
for e in ecarts:
print(" -", e)
return 1
note = f" ({len(absents)} non declare(s), donc derive(s) ou non utilise(s))" if absents else ""
print(f"CONFORME : {len(vu)} intrant(s) du locataire concordent avec ce que le site expose{note}.")
return 0
def main(argv: list[str]) -> int:
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("--json", action="store_true")
ap.add_argument("--verifier", action="store_true")
args = ap.parse_args(argv)
if args.verifier:
return verifier()
c = contrat()
if not c:
print("Aucun site monte (`underlay.yml` absent ou brise).")
return 0
if args.json:
print(json.dumps(c, indent=2, ensure_ascii=False))
return 0
print(f"# Intrants exposes par le site « {c['domaine_site'] or '?'} » (index {c['index_site']})")
print("#")
print("# A recopier dans les intrants d'un locataire qui veut l'habiter — ou mieux :")
print("# a comparer aux siens par `--verifier`, qui dit lesquels ont pris du retard.")
for clef, valeur in c.items():
if clef in ("domaine_site", "index_site", "forge_amont_ac"):
continue
v = json.dumps(valeur, ensure_ascii=False) if isinstance(valeur, list) else valeur
print(f"{clef}: {v}" if valeur else f"# {clef}: (le site ne porte pas ce service)")
print(f"#\n# racine de confiance de la forge : {c['forge_amont_ac']}")
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))