Set-OPS-Public/docs/plan-et-generation.md
Daniel Allaire 2887b57f0c GUI : les six registres ont un formulaire genere, et la sauvegarde aussi
CHAMPS_ECRITS_A_LA_MAIN est vide. Serveurs et applications, les deux plus
gros, sont passes au generateur — chargement, rendu et sauvegarde.

L EPREUVE QUI COMPTE. Ouvrir chaque vue et enregistrer sans rien toucher
doit renvoyer exactement le plan qu on vient de lire : 14 serveurs, 25
applications, 2 domaines, 4 bases, IDENTIQUE partout. C est ce qui separe
un formulaire genere d un formulaire qui en a l air — un champ visible a
l ecran et perdu en silence a l enregistrement serait le pire des deux
mondes. test_rendu_gui.py le mesure a chaque make prouver.

TROIS DEFAUTS TROUVES EN CHEMIN.

Le formulaire annoncait des defauts INVENTES : 2048 Mo, 2 coeurs, 16G. Il
n existe aucun defaut fixe — deriver_ressources calcule depuis les roles
portes (1024 et 1 pour infra-pki-01, 5632 et 4 pour collab-01). Un repere
faux fait croire qu on connait la valeur. Le schema nomme le champ derive,
et l ecran montre la valeur reelle de cet hote. L option vide d un select
dit desormais ce qu elle produira : « (defaut : asgard) ».

Une SECONDE occurrence du defaut d hier dormait dans sourceDeValeurs :
elle lisait encore data.nomenclature. Elle n avait jamais leve parce que
la vue Serveurs, seule a emprunter cette source, avait un formulaire ecrit
a la main. Elle a leve a la seconde ou le generateur l a prise. Le banc ne
voit que les chemins vivants : verifier_gui.py fait donc aussi une
verification STATIQUE, qui voit ce qui dort.

La validation client s accrochait a data-v, pose a la main sur trois
champs. Le formulaire genere l aurait perdu et la validation serait passee
au vert sur ZERO champ. Le generateur marque chaque controle, et la
sauvegarde refuse si elle n en inspecte aucun.

DEUX CHAMPS GARDENT LEUR EDITEUR, et le schema le dit (x-editeur) : la
matrice des integrations montre les universelles et les exemptions, et
l editeur de liens contraint le role a meta/liens.yml. Le generateur s
efface plutot que de remplacer un editeur qui en sait plus que lui.

LIMITE : je n ai toujours pas ouvert ces pages dans un navigateur.

make prouver : CONFORME, 61 OK, 0 echec, 1 saute.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 18:26:33 -04:00

211 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Le plan et la génération d'inventaire (méta-classe)
> **Pour qui :** le **mainteneur** — le plan et la génération de l'inventaire, à fond.
Set-OPS ne s'édite plus comme un inventaire à la main : on **décrit un plan**, et
l'inventaire Ansible en est **généré**. Le dépôt est la définition ; chaque VM en
est une instance. Ce document décrit le modèle, les registres, les commandes et
le flux de travail.
> Règle d'or : **`instance/inventories/<inventaire>/hosts.yml` est GÉNÉRÉ. Ne jamais l'éditer
> à la main.** On édite le *plan* puis on régénère (`make instancier-appliquer`).
> **`<inventaire>` n'est pas un nom, c'est une place.** Le dépôt n'impose pas comment une
> instance nomme son inventaire : **le moteur le cherche**, dans l'ordre `principal`, puis
> `production` (`scripts/inventory_rules.py` : `ORDRE_INVENTAIRE` ; pour la construction du
> gabarit, `ORDRE_INVENTAIRE_MODELE` essaie `lab` d'abord). La flotte utilise `principal` ;
> le modèle public livré dans `exemples/modeles/socle/` utilise `production`, et le
> QUICKSTART l'écrit tel quel parce que c'est ce que son lecteur a sous la main. Les
> documents de doctrine, eux, écrivent `<inventaire>` : coder l'un des deux noms en dur y
> serait faux pour la moitié des lecteurs — et ça l'a été jusqu'au 2026-09-06.
---
## 1. Le modèle : deux ancres, cinq liens
L'écosystème = des **serveurs** (VM) et des **applications**, reliés à leurs
**bases**, leurs **domaines** et leurs dépendances :
```
serveur (VM) ──fournit──▶ capacités (= groupes/rôles Ansible)
application ──tourne_sur──▶ serveur (une VM peut porter N applications)
application ──requiert──▶ application(s) (DNS, PKI, BD…)
application ──utilise──▶ base(s) (via le DSN)
application ──expose──▶ domaine(s) public(s) (le FQDN, derrière l'edge)
base ──hébergée_sur──▶ serveur de BD
```
L'**application** est l'entité pivot : tout ce qui décrit « ce qui tourne et à
quoi c'est connecté » pend d'elle. Le **groupe** Ansible n'est plus une cible de
liaison — seulement une **capacité** qu'une VM fournit (le rôle appliqué).
---
## 2. Les registres (source unique de vérité)
Machine-lisibles, validés, consommés par le GUI, le CLI et Ansible. **Ils vivent dans
l'instance** (`instance/plan/`), pas dans le moteur — c'est toute la séparation
moteur/instance. Seul `dependances-groupes.yml` est sous `docs/`, parce qu'il décrit une
propriété des *rôles*, la même pour toutes les instances.
| Registre | Décrit | Champs clés |
| --- | --- | --- |
| `nomenclature.yml` | nommage & adressage | **`index`** (le seed, seul champ d'adressage), `fonctions` (catégorie/service), `categories` (libellés de zones), `cidr_hote`, `reservations`. **Ni `supernet`, ni `vlan`, ni `passerelle` : P20 les refuse** — ils se dérivent. |
| `serveurs.yml` | les VM du plan | `fonction`, `etat` (actif/planifie), placement Proxmox (`noeud`/`stockage`/`disque`/`memoire`/`coeurs`), `integrations` (les `client_*` **facultatives** seulement — les universelles viennent du rôle, voir `integrations-vm.md`) |
| `applications.yml` | les applications | `groupe` (capacité/rôle), `hote` (VM), `port`, `requiert`, `expose`, (+ bases via consommateur) |
| `bases-donnees.yml` | serveurs de BD + bases | `serveurs_bd` ; `bases_donnees` : `serveur`/`base`/`proprietaire`/`secret`(Vault), `consommateur` + `portee` (`application`/`groupe`/`hote`), `usage` |
| `domaines.yml` | zones DNS publiques | `domaines_publics` : `autorite`, `edge`, `secondaires`, `dnssec`, `mail` |
| `dependances-groupes.yml` | prérequis entre groupes | `requiert_groupes_actifs` |
### Dérivations clés
- **Nommage/adressage** : tout part du seed `index` et de la `fonction` de l'hôte.
`supernet = 10.<index>.0.0/16`, `zone = 10.<index>.(15+catégorie)`,
`VLAN = 1000 + index×10 + zone`, `VMID = <VLAN><octet-hôte><rang>` — neuf chiffres,
miroir de l'IP. Voir `docs/nomenclature-vm.md`. *(Les formules à cinq chiffres et le
réseau unique `10.0.x` qui figuraient ici décrivaient le modèle d'avant le
multi-instance.)*
- **DSN** (lien application↔base) : `<type>://<proprietaire>:<secret>@<hôte>:<port>/<base>`.
Une application reçoit les bases où `(portee=application ET consommateur=elle)`
OU `(portee=groupe ET consommateur=son groupe)` OU `(portee=hote ET consommateur=son hôte)`.
- **Exposition DNS** : `application.expose: [fqdn]` + l'`edge` du domaine parent →
`serveur_nginx` génère le vhost (`application → hôte → IP:port`).
---
## 3. La génération (méta-classe)
`make instancier` produit `hosts.yml` **depuis le plan** :
- **host vars** : `ansible_host`/`ansible_user` + `proxmox_*` (IP/VMID/VLAN/passerelle
**dérivés** de la nomenclature ; placement/taille depuis `serveurs.yml`).
- **groupes** d'une VM = socle (`serveur_debian` + `serveur_durci`)
+ **services** (les `groupe` des applications de l'hôte)
+ **intégrations universelles** (politique du rôle : `roles/client_*/meta/integration.yml`)
+ **intégrations facultatives** (`serveurs.yml: integrations`)
+ **état** (`hotes_actifs` / `hotes_planifies`).
La comparaison est **sémantique** (via `ansible-inventory --list`, formatage
ignoré). « **Diff vide** » = le plan reproduit exactement l'inventaire courant ;
c'est le feu vert pour appliquer.
---
## 4. Le flux de travail
```
éditer le PLAN ──▶ make instancier (revoir le diff) ──▶ make instancier-appliquer ──▶ déployer
(GUI ou CLI) (que va-t-il changer ?) (régénère hosts.yml) (make deployer)
```
### Via le GUI — `make inventaire-ui`
**Douze vues**, éditables ou dérivées :
| Vue | Rôle |
| --- | --- |
| **Serveurs** *(éditable)* | les VM du plan : fonction/état/placement/intégrations (VMID·IP·VLAN dérivés en direct) ; bouton **« Appliquer le plan »** |
| **Applications** *(éditable)* | groupe/hôte/port/requiert/expose, et les **liens** acceptés par le rôle |
| **Bases** *(éditable)* | serveurs de BD et bases (portée + consommateur), DSN affiché (secret masqué) |
| **Domaines** *(éditable)* | zones publiques vs internes, autorité · edge · DNSSEC, expositions |
| **Nomenclature** *(éditable)* | le modèle dont tout l'adressage dérive : zones, fonctions (catégorie · service), réservations. Chaque fonction affiche **ce qu'elle dérive** (VLAN, sous-réseau, bloc d'hôtes) et les VM qui la portent. L'`index` y est **montré, pas éditable** : il est alloué par le site |
| **Intégrations** *(éditable)* | la matrice serveurs × intégrations ; les universelles en ✓ non décochables |
| **Intrants** *(éditable)* | les intrants de base — identité, Proxmox, fabric, et la liste de rappel des secrets (lecture seule) |
| **Inventaire** *(lecture seule)* | l'inventaire **généré** — vue d'ensemble des hôtes |
| **Chaîne** *(lecture seule)* | vue holistique par hôte : groupes → rôles, et par application ses `expose` / `requiert` / bases |
| **Flux** *(lecture seule)* | la matrice d'audit des flux — source de nftables et justification lisible |
| **Couches** *(lecture seule)* | l'ordre de déploiement en six couches |
| **Réseau** *(lecture seule + bascule)* | la flotte multi-instances, les collisions d'index, et le bouton **« Activer »** |
Édition → **Sauvegarder** (écrit le registre) → **Appliquer le plan** (régénère
`hosts.yml`). L'écriture directe de l'inventaire est refusée (409).
**Les formulaires des six registres sont générés** depuis `docs/audit/schema-plan.json`
(`make schema`), dérivé des constantes du moteur. La sauvegarde en dérive aussi : un
champ ajouté au plan apparaît à l'écran *et* arrive au fichier. Deux exceptions
déclarées au schéma (`x-editeur`) : la matrice des intégrations et l'éditeur de liens
gardent leur éditeur propre, plus riche que ce que le schéma sait dire.
### Via le CLI / `make`
Chaque registre a son script miroir et ses cibles `make` :
| Domaine | Lister | Vérifier | Bootstrap (depuis l'inventaire) |
| --- | --- | --- | --- |
| Serveurs | `make serveurs` | `make serveurs-verifier` | `make serveurs-bootstrap` |
| Applications | `make applications` | `make applications-verifier` | `make applications-bootstrap` |
| Bases | `make bases` | `make bases-verifier` | — |
| Domaines | `make domaines` | `make domaines-verifier` | — |
Génération :
```bash
make instancier # génère hosts.genere.yml (gitignoré) + diff sémantique
make instancier-appliquer # régénère hosts.yml (refuse si diff non vide ; FORCE=1 pour forcer)
```
Validation globale : `make inventaire-verifier` (ansible-inventory + tous les
registres + **`node --check` du JS du GUI**).
---
## 5. Garde-fous
- **Validateurs** : chaque registre est validé (références connues, énumérations,
unicité). Un `expose` sans domaine parent, un `requiert` fantôme, une `portee`
inconnue, une `fonction` absente → rejet.
- **Diff vide** : `instancier-appliquer` refuse d'écraser l'inventaire si le plan
ne le reproduit pas (sauf `FORCE=1` pour un changement intentionnel).
- **`node --check`** : le JS embarqué du GUI est vérifié (`scripts/verifier_gui.py`,
intégré à `make inventaire-verifier`) — une erreur de syntaxe JS casse toute la page.
- **git** : `hosts.yml` est versionné ; `git diff` / `git checkout` est le filet.
- **Secrets** : jamais en clair ; `secret:` nomme une variable Ansible Vault.
---
## 6. Réutilisation de la règle (Ansible)
La règle de résolution vit **une seule fois**, en Python (`scripts/inventory_rules.py`),
et est exposée à Ansible par un *filter plugin* (`filter_plugins/registres.py`) :
`bases_de_application`, `applications_de_hote`, `expositions_des_applications`,
`chaine_connexion`. Les playbooks par application (`serveur_web_dorsal`, `serveur_web_frontal`)
itèrent ainsi sur les applications de l'hôte et résolvent leurs DSN.
---
## 7. Amorçage / reconstruction
Pour (re)construire le plan depuis un inventaire existant :
```bash
make serveurs-bootstrap # VM -> instance/plan/serveurs.yml (fonction/état/placement/integrations)
make applications-bootstrap # services serveurs_* (hors socle) -> instance/plan/applications.yml
make instancier # vérifier le diff vide
```
C'est ainsi que le plan a été initialisé sans perte, avec diff vide vérifié.
---
## 8. Moteur et instance : deux dépôts
Le **moteur** (ce dépôt, `Set-OPS`) est générique et partageable ; il ne contient
aucune donnée d'instance. Une **instance** (le plan + l'inventaire d'une organisation) vit
dans son **propre dépôt** (ex. `OPS-monatelier`).
Le moteur localise l'instance via **`SETOPS_INSTANCE`** (défaut : `instance`). Deux
modèles :
- **Modèle A — dépôts frères** (en cours) : moteur et instance côte à côte ; un
symlink `instance -> ../OPS-monatelier` (gitignoré) fait que le défaut résout
l'instance sans configuration. Idéal quand on développe le moteur *et* l'instance.
- **Modèle B — moteur en sous-module** (futur, quand plusieurs écosystèmes vivront chez des exploitants distincts) : l'instance épingle
une version du moteur ; `SETOPS_INSTANCE` pointe la racine de l'instance.
Pour brancher une instance (modèle A) :
```bash
cd Set-OPS
ln -s ../OPS-monatelier instance # ou : export SETOPS_INSTANCE=/chemin/instance
make inventaire-verifier # lit l'instance via le symlink
```
Le moteur écrit `hosts.yml` (généré) **dans le dépôt d'instance**, jamais dans le sien.