diff --git a/AGENTS.md b/AGENTS.md index a8e87de..adb91a9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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.** diff --git a/CHANGELOG.md b/CHANGELOG.md index e4e16fa..07a1b8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Makefile b/Makefile index 9e0a0c6..fe53c5e 100644 --- a/Makefile +++ b/Makefile @@ -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,) diff --git a/docs/audit/preuve-2026-09-13.md b/docs/audit/preuve-2026-09-13.md index 127b65a..7c287b8 100644 --- a/docs/audit/preuve-2026-09-13.md +++ b/docs/audit/preuve-2026-09-13.md @@ -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 diff --git a/docs/devis-services.md b/docs/devis-services.md index 51d0460..698e994 100644 --- a/docs/devis-services.md +++ b/docs/devis-services.md @@ -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. diff --git a/scripts/prouver.py b/scripts/prouver.py index 569317d..e7e159c 100644 --- a/scripts/prouver.py +++ b/scripts/prouver.py @@ -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}, ] diff --git a/scripts/site_intrants.py b/scripts/site_intrants.py new file mode 100644 index 0000000..230cb46 --- /dev/null +++ b/scripts/site_intrants.py @@ -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:]))