diff --git a/CHANGELOG.md b/CHANGELOG.md index c5931cf..f11da6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,33 @@ # CHANGELOG — Set-OPS +## 2026-10-04 (78) — Conception : un tronc commun, deux classes (SITE et LOCATAIRE) + +**Demande de l'exploitant** : en finir avec les « classes à tout faire ». Deux grandes classes, +SITE et LOCATAIRE, qui héritent du tronc commun Set-OPS et surchargent ce qui dépend de leur +contexte ; des consoles cohérentes avec leur contexte. + +**Page arrêtée avec lui** : `docs/conception-contextes.md`. Rien n'est encore construit. + +**Le relevé qui la fonde** : +- 33 scripts devinent leur contexte, à partir de cinq indices (liens `instance/` et + `underlay.yml`, variables `SETOPS_*`, dossiers frères, `SITE-*`). Trois défauts passés en + viennent, dont le `make ci` mélangé du jour. +- Le site informe le locataire par **trois canaux**, sans fiche : des copies à la main (douze + valeurs, un certificat, une valeur hors contrat), une **lecture directe du site par + l'instancier** (mesurée : sans le site monté, l'inventaire de Technolibre perd son serveur de + temps, son SDN et sa délégation DNS), et le génome. +- Le site **fouille** six fichiers internes du locataire et **recalcule** ses flux avec sa + propre version du moteur. + +**Décisions** : `Ecosysteme` → `Site`, `Locataire` ; un **contrat** en deux fiches générées +(la fiche du site pour chaque locataire, **déposée par le site** dans le dépôt du locataire ; +la face réseau du locataire, flux déjà résolus compris) ; le poste devient un **sélecteur** ; +le contexte actif se nomme dans un fichier `contexte` ; `SITE-Modele` à côté d'`OPS-Modele` ; +un `parente.yml` pour le site. On commence par le moteur. + +**Ce que ça donne en plus** : la portabilité. Un locataire qui change de site reçoit la fiche +du nouveau, et son dépôt ne contient plus rien d'interne à l'ancien. + ## 2026-10-04 (77) — Release `v2026.10.04` : les deux locataires reconstruits sur `f42d30b` **Pourquoi une seconde fois le même jour.** (72) prouvait `5340220`. Ensuite, (73) et (74) ont diff --git a/docs/conception-contextes.md b/docs/conception-contextes.md new file mode 100644 index 0000000..9ffd388 --- /dev/null +++ b/docs/conception-contextes.md @@ -0,0 +1,259 @@ +# Contextes : un tronc commun, deux classes (SITE et LOCATAIRE) + +> **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`). +- **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é. + +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.