# Contextes : un tronc commun, deux classes (SITE et LOCATAIRE) > **Pour qui :** le **mainteneur** — comment le moteur sait s'il sert un site ou un locataire, et ce que les deux s'apprennent l'un à l'autre. > **Statut : arrêtée avec l'exploitant le 2026-10-04.** Rien n'est encore construit ; les > décisions sont au §7, le chemin au §6. ## 1. Le problème, mesuré Le moteur ne sait pas dans quel contexte il tourne : **chaque script le devine**. Relevé du 2026-10-04 : **33 scripts** font leur propre déduction, à partir de cinq indices différents. | Indice | Ce qu'on en déduit | Scripts | |---|---|---| | lien `instance/` ou `SETOPS_INSTANCE` | « un locataire est monté » | 23 | | lien `underlay.yml` ou `SETOPS_UNDERLAY` | « un site est monté » ; son plan est à côté | 8 | | `SETOPS_INVENTAIRE` | quel inventaire de locataire lire | 8 | | `../*/plan/nomenclature.yml` | « la fédération », les locataires frères | 11 | | `../SITE-*/underlay.yml` | « les sites » | 2 | Les indices ne concordent pas toujours, et chaque désaccord a déjà produit un défaut silencieux : - **2026-08-14** : `frontiere-plan` voulait poser sur la frontière de Technolibre les règles de Chezlepro. « La fédération » valait « les locataires de ce site », jusqu'au second site. - **2026-09-16** : la console du runner du site affichait zéro machine, sans erreur. Elle cherchait un inventaire de locataire là où il n'y en a pas. - **2026-10-04** : `make ci`, sur le poste, mélangeait le modèle public et les écosystèmes réels. P74 lisait `SETOPS_UNDERLAY` (le modèle) ; P82 lisait le lien `underlay.yml` et les dossiers frères (le site réel). Les rôles Ansible, eux, ne posent pas ce problème : ils sont **déjà** le tronc commun. Un même `serveur_postgresql` sert au site et chez un locataire. ## 2. Le modèle ``` Ecosysteme (tronc commun) / \ Site Locataire \ / `-- contrat --' (associations : un site A des locataires, un locataire A un site) ``` ### 2.1 Le tronc commun : `Ecosysteme` Ce que tout écosystème possède, quel que soit son contexte : - un **nom** et un **dépôt** (`SITE-Chezlepro`, `OPS-Technolibre`) ; - une **voûte** et sa clé (`~/.config/setops-vault-`) ; - un **plan** (`/plan/`) et un **index**, dont dérive son adressage (le site aussi depuis le 2026-09-20) ; - des **machines**, déployées par les **mêmes rôles** : socle, durcissement, PKI, journaux, métriques, supervision, sauvegarde de son propre état ; - une **filiation** : le moteur et le commit dont il descend ; - les **preuves communes** : lint, rendu des gabarits, adressage dérivé, etc. Méthodes abstraites, que chaque classe **surcharge** : `inventaire()`, `machines()`, `preuves()`, `verbes()`, `console()`. ### 2.2 `Site(Ecosysteme)` - **Déclaration** : `underlay.yml` (le matériel, les réseaux `site` et `fabric`) et `plan/` (`10-intrants.yml`, serveurs, applications, domaines, bases). - **Inventaire** : dynamique (`site_inventaire.py`). Le site ne dérive rien d'un plan de services ; sa déclaration est sa forme finale. - **Ce qu'il porte en propre** : le matériel (hyperviseurs, commutateurs, frontière), la matérialisation des VM (Proxmox), le SDN, le pare-feu Proxmox, la frontière OPNsense, le DNS public, le dépôt des sauvegardes des locataires, le cache et les artefacts, la forge du génome. - **Relation** : `locataires()`, la liste de `underlay.tenants` résolue en objets `Locataire`. Le site n'en lit que la **face réseau** (§2.4). ### 2.3 `Locataire(Ecosysteme)` - **Déclaration** : `plan/` (nomenclature, serveurs, applications, bases, domaines). - **Inventaire** : généré (`instancier.py` → `hosts.yml`). C'est la **méta-classe** de [`meta-classe.md`](meta-classe.md) : une définition qui engendre toute la flotte. - **Ce qu'il porte en propre** : la configuration de ses services, la remise au client. - **Relation** : `site()`, l'hébergeur que nomme `parente.yml`, résolu en objet `Site`. Le locataire n'en lit que les **intrants exposés** (§2.4). ### 2.4 Ce que le site et le locataire s'apprennent l'un à l'autre Les deux entités **s'informent mutuellement**. Relevé du 2026-10-04 : qui décide de chaque information, où elle vit, et comment elle parvient à l'autre. **Ce que chacun a sous la main.** Le runner du site porte le moteur, son dépôt **et ceux de ses locataires** (sans leurs voûtes). Le runner d'un locataire ne porte que le moteur et **son propre** dépôt. Le poste porte tout. #### Le site informe le locataire, par trois canaux **Canal 1 : des copies écrites à la main** dans le dépôt du locataire. | Information | Décidée par | Tenue chez le site dans | Copiée chez le locataire dans | Contrôle | |---|---|---|---|---| | son **index** | le site | `underlay.yml` → `tenants` | `plan/nomenclature.yml` (`index`) | `underlay valider` | | son **adresse publique** | le site | `opnsense.yml` → `opnsense_ips_publiques` | `10-intrants.yml` (`ip_publique`) | — | | les **10 intrants de service** : résolveur, cache, binaires, forge du génome, cible de sauvegarde, DNS public, plan d'administration, passerelle | le site (dérivés de son plan, par `site_intrants.py`) | son plan | `10-intrants.yml`, et `serveur_ops.yml` pour la forge | `site_intrants.py --verifier`, seulement là où les deux dépôts sont présents (le poste) | | sa **racine de confiance** | le site | `ac-racine-site.crt` | le même fichier, copié | — | | *hors contrat* : un dépôt de la forge du site désigné par son adresse | — | — | `serveur_web_dorsal.yml` (Chezlepro) | **aucun** | **Canal 2 : une lecture directe, au moment de générer l'inventaire.** `instancier.py` ouvre l'`underlay.yml` et le plan du site pour écrire le `hosts.yml` du locataire. Mesuré sur Technolibre, inventaire généré avec puis sans le site monté : **quatre variables changent**. | Variable du locataire | Avec le site monté | Sans le site | |---|---|---| | `chrony_serveurs` | `10.0.4.1` (la frontière) | absente | | `proxmox_pont` | `t23appl` (le VNet SDN) | absente | | `proxmox_etiquette_vlan` | aucune (le SDN étiquette) | `1236` | | `serveur_resolveur_zones_deleguees` | `genese.internal` → `10.37.34.11` | absente | **Canal 3 : le réseau.** Le runner du locataire **tire** son génome de la forge du site ; il est né de l'**insémination** par le runner du site. #### Le locataire informe le site : le site lit et recalcule | Information | Décidée par | Tenue chez le locataire dans | Parvient au site par | |---|---|---|---| | ses **zones** et son adressage | dérivés de l'index | `plan/nomenclature.yml` | le site **lit le fichier** | | les **VM à matérialiser** | le locataire | `plan/serveurs.yml` → `hosts.yml` | le site **lit les fichiers** (placement, clonage, pools, SDN) | | ses **flux** | ses rôles et son plan | `meta/flux.yml` des rôles (moteur), croisés avec son `hosts.yml` | le site **recalcule** lui-même, avec **sa** version du moteur et la totalité de l'inventaire du locataire → frontière, NAT, pare-feu Proxmox | | ses **domaines publics** | le locataire | `plan/domaines.yml` (+ `applications.yml`, `serveurs.yml`) | le site **lit les fichiers** → DNS public secondaire | | sa **clé de sauvegarde** | le locataire | `inventories/*/group_vars/serveur_backup.yml` | le site **lit le fichier** → compte Unix sur le dépôt | | ses **accès d'administration** | le locataire | `plan/acces.yml`, `nftables_admin_ssh` | le site **lit les fichiers** → pairs WireGuard, règles d'administration | Le SDN, lui, ne prend aucun flux : il ne filtre pas. Il ne reçoit que l'index, dont il dérive la zone, les 6 VNets et les 6 sous-réseaux. #### À l'exécution, entre machines Ces échanges-là passent par le réseau, pas par les dépôts. Ils sont déjà déclarés en flux : le locataire **dépose** ses sauvegardes chez le site (SFTP), **tire** ses paquets, ses binaires et son génome, **entre** par le tunnel d'administration du site ; le site **réplique** les zones publiques du locataire (AXFR signé TSIG). #### Ce que le relevé montre 1. **Le site fouille l'intérieur du locataire.** Six fichiers de son plan et de son inventaire, `group_vars` compris. Rien ne dit ce que le locataire **accepte** de montrer. Renommer un champ chez le locataire casse le site sans bruit. 2. **Les flux sont calculés deux fois**, par le locataire pour ses `nftables` et par le site pour la frontière et Proxmox, chacun avec **sa** version du moteur. Ils concordent tant que les deux runners tiennent le même commit (c'était le cas le 2026-10-04), mais rien ne l'impose. 3. **Le locataire vit de copies** : douze valeurs et un certificat, recopiés à la main, plus une valeur hors contrat. La garde qui compare ne tourne que sur le poste ; le runner du locataire ne peut pas savoir que sa copie a vieilli. 4. **L'inventaire d'un locataire dépend du site monté au moment de le générer.** Généré sur le runner du locataire, qui n'a pas le dépôt du site, il perdrait son serveur de temps, son SDN et sa délégation DNS. Ça ne s'est jamais vu, parce que l'inventaire est toujours généré sur le poste puis versionné. 5. **Deux décisions du site** (l'index, l'adresse publique) vivent en double. #### Proposition : deux fiches, une dans chaque sens Chacun **publie** ce qu'il donne à l'autre, dans une fiche **générée** par le moteur, jamais écrite à la main. Chacun ne lit que la fiche que l'autre lui destine. Plus aucune lecture croisée, plus aucun recalcul. - **La fiche du site pour un locataire.** Tout ce que le site lui **attribue** (index, adresse publique) et lui **offre** : les 10 intrants, sa racine de confiance, et ce que l'instancier allait lire en douce (serveur de temps, délégation DNS, mode SDN et nom des VNets). Une fiche **par locataire** : aucun ne voit le plan du site ni ses voisins. L'instancier ne lit plus que cette fiche, et l'inventaire devient **identique où qu'on le génère**. - **La face réseau du locataire.** Ce qu'il **demande** au site : VM à matérialiser, zones, domaines publics, clé de sauvegarde publique, accès d'administration, et **ses flux déjà résolus** (adresses, ports, protocoles), ceux avec l'extérieur pour la frontière et ceux de chaque VM pour Proxmox. Le locataire génère déjà ses flux résolus (`flux-genere/*.nft`, `*.connectivite.json`) : la face réseau en est la partie destinée au site. Le site ne recalcule plus rien : il applique ce que le locataire publie, après l'avoir confronté à sa propre politique. - **Chaque fiche porte l'empreinte de sa source**, et une preuve de chaque côté vérifie que la fiche reçue correspond à ce que l'autre a publié. **Où en est l'étape 2 (2026-10-04).** La fiche du site (P84), les faits de la face réseau (P85) et les flux de chaque machine (P86) existent, et disent exactement ce que les lectures croisées produisent. La première mesure des flux a trouvé une information que le locataire jetait : les clients nommés d'un port aussi public, que Proxmox doit admettre nommément. Il les publie désormais (`sources_declarees`). La frontière, en trois temps : les identités (P87), les entrées publiques (P88), l'administration (P89) et les sorties (P90) sont faites. **L'étape 2 est terminée** (2026-10-05). Étape 3 : l'instancier lit la fiche déposée par le site, et l'inventaire d'un locataire se génère sans son site, à l'octet près (P91). Le locataire publie sa face réseau (P92) ; les comptes de sauvegarde, le DNS public, le pare-feu Proxmox, la frontière et la découverte des locataires du site la lisent. La matérialisation aussi : la face publie les paramètres de clonage (P93) ; `locataire-creer`, `locataire-raser` et `placement-plan TENANT=` nomment leur locataire au lieu de le monter, et visent les mêmes machines, à l'argument près de la ligne `ansible-playbook` (P94, `test_appels_locataire.py`) ; `reconstruire-locataire` les emploie. Reste : la preuve par reconstruction. Méthodes du contrat : `site.fiche_pour(locataire)`, `locataire.face_reseau()`. Une classe n'ouvre jamais les fichiers de l'autre ; une preuve vérifiera la règle. #### Ce que les fiches donnent : la portabilité Un locataire qui change de site, pour un déménagement, un plan de reprise ou une émancipation, n'a plus qu'à **recevoir la fiche de son nouveau site**. Son dépôt ne contient plus rien d'interne à l'ancien : ni copie d'adresse, ni inventaire généré avec l'ancien site monté. En face, le nouveau site n'a qu'à lire sa **face réseau**. Aujourd'hui, la même bascule demande de corriger des copies dans plusieurs fichiers, puis de régénérer l'inventaire avec le nouveau site monté sur le poste. ## 3. Le poste : un sélecteur Aujourd'hui, le poste monte les deux contextes **en même temps** (`instance/` + `underlay.yml`), et `ConsolePoste` hérite de `ConsoleLocataire`. Désormais : - **Le poste choisit un contexte actif** : un site **ou** un locataire. La console ouvre celui-là, et seulement celui-là. - **Une opération qui traverse les deux** nomme ses objets au lieu de les deviner. `reconstruire-locataire` en est l'exemple : le site matérialise, puis le locataire monte. L'orchestration devient `site.materialiser(locataire)` puis `locataire.monter()`. - **Les runners ne changent pas** : celui du site n'a qu'un `Site`, celui d'un locataire qu'un `Locataire`. Leur contexte est désormais **dit**, plus déduit. ## 4. Qui surcharge quoi | Méthode | Tronc commun | Site | Locataire | |---|---|---|---| | `inventaire()` | abstraite | script dynamique (`underlay.yml`) | `hosts.yml` généré du plan | | `machines()` | abstraite | VM du site + équipements | VM du plan | | `adressage()` | dérivé de l'index | zones `site` dérivées, liens `fabric` écrits | 6 zones dérivées | | `sauvegardes()` | son propre état, vérifié par restauration | + héberge les dépôts des locataires | dépose chez son site | | `supervision()` | sondes déclarées par les rôles | + matériel, fabric, frontière | — | | `raser()` / `reconstruire()` | — | `site_raser.py` | `raser.py`, `reconstruire_locataire.py` | | `preuves()` | preuves communes | + preuves de site (P23, P74, P82…) | + preuves de locataire | | `verbes()` | `verifier`, `publier`… | `site-*`, `frontiere-*`, `proxmox-*` | `appliquer`, `instancier`, `remise-*` | | `console()` | — | console SITE | console LOCATAIRE | ## 5. Ce qui ne change pas - Les **rôles Ansible**, déjà communs. - Les **formats de plan** : pas dans ce chantier. - La **doctrine** d'`AGENTS.md`. ## 6. Le chemin, chaque pas prouvé avant le suivant 1. **`scripts/contexte.py`** : `Ecosysteme`, `Site`, `Locataire`, `contexte_actif()`, `Site.charger(nom)`, `Locataire.charger(nom)`. Tests unitaires. Rien ne l'utilise encore. 2. **Les deux fiches**, générées à côté de l'existant sans rien remplacer : `site.fiche_pour(locataire)` et `locataire.face_reseau()`. Une preuve vérifie que chaque fiche dit **exactement** ce que les lectures croisées d'aujourd'hui produisent. 3. **Les consommateurs basculent sur les fiches**, un par un : l'instancier sur la fiche du site (l'inventaire généré doit rester identique, octet pour octet) ; la frontière, Proxmox, le DNS public et les comptes de sauvegarde sur la face réseau (chaque devis doit rester inchangé). 4. **`prouver.py`** : chaque preuve déclare son contexte (commun, site, locataire) et reçoit son écosystème du module. 5. **Les autres scripts**, un par un, vérifiés par `make verifier` et par un devis inchangé. 6. **Une preuve « aucune devinette, aucune lecture croisée »** : les indices du §1, et toute ouverture d'un fichier de l'autre contexte, interdits hors de `contexte.py`. 7. **Les verbes du Makefile** rangés par contexte. 8. **Les consoles** : le sélecteur et deux consoles (chantier suivant). 9. **OPS-Modele** : un locataire modèle, et sans doute un site modèle, vérifiés chacun dans son contexte. Après les étapes 3 et 5, une reconstruction prouve que la flotte n'a pas bougé. ## 7. Décisions et questions ouvertes ### Tranché par l'exploitant - **Deux classes, `Site` et `Locataire`, qui héritent d'un tronc commun** (2026-10-04). - **Le poste est un sélecteur** : un contexte actif à la fois (2026-10-04). - **On commence par le moteur**, les consoles viennent ensuite (2026-10-04). - **Le site dépose sa fiche dans le dépôt du locataire** (2026-10-04), comme il y amorce déjà son runner. Le runner du locataire n'a besoin d'aucun accès au dépôt du site, et ne voit ni le plan du site ni ses voisins. - **Le contexte actif se nomme dans un fichier `contexte` explicite** (2026-10-04), une seule valeur : `site:SITE-Chezlepro` ou `locataire:OPS-Technolibre`. Un sélecteur qui monte deux liens à la fois contredirait sa propre règle. Les liens `instance/` et `underlay.yml` restent le temps de la bascule, lus par le seul `contexte.py`. - **Un modèle SITE public, `SITE-Modele`**, à côté d'`OPS-Modele` (2026-10-04). Sans site, la CI ne peut exercer ni les preuves de site (P74, P81, P82) ni la fiche que le site dépose chez le locataire. - **Le site a aussi son `parente.yml`** (2026-10-04) : la filiation est dans le tronc commun.