diff --git a/CHANGELOG.md b/CHANGELOG.md index 241ed86..99ee4fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,44 @@ # CHANGELOG — Set-OPS +## 2026-08-12 — `modeleSetOPS`, et une porte pour l'hébergeur + +### Le gabarit portait le nom du mauvais propriétaire + +`modeleChezlepro` était déclaré par les **trois** instances — Chezlepro, Technolibre et le +lab — qui pointaient déjà toutes sur le **même** VMID 99998. Le commentaire du rôle +affirmait pourtant « chaque tenant a SON golden template » : c'était faux depuis +longtemps, et personne ne pouvait le voir en lisant un seul fichier. + +Renommé **`modeleSetOPS`** — sur le cluster et dans les trois instances. Le gabarit est un +artefact du **moteur**, pas d'un tenant, et le nom d'un tenant sur le gabarit d'un autre +était un piège qui n'attendait qu'un troisième hébergeur pour se refermer. Les scripts +d'amorçage (`model_creer.py`, `config_proxmox.py`) proposaient encore +`modele-debian13` : alignés eux aussi. + +Sans risque : le clonage se fait **par VMID** depuis la correction du 2026-08-10 — le nom +ne sert plus qu'à l'affichage et aux vérifications. Rien n'empêche un tenant d'en désigner +un autre ; il change le champ **et** le VMID. + +### Une porte de plus dans l'aiguillage : l'hébergeur + +`docs/preparer-un-site-hebergeur.md` — pour quelqu'un qui **prête son matériel** sans rien +connaître de Set-OPS. Il ne décrit que ce que la machine ne peut pas deviner : le plan +d'adressage à respecter, la frontière, le stockage, l'hyperviseur, le gabarit, et la liste +exacte de ce qu'il doit transmettre en retour. + +Écrit à partir du dépôt, pas de conventions générales : les VLAN et MTU viennent +d'`underlay.yml`, le bloc par tenant de `inventory_rules.supernet_de()`, les privilèges du +jeton de `config-proxmox.md`, et le dimensionnement (**~460 Go, ~37 Go de RAM pour +quatorze VM**) d'une mesure sur la flotte vivante. + +Deux avertissements y sont écrits parce qu'ils ont déjà coûté cher ici : **un gabarit +personnalisé recopie son identité dans chaque clone**, et **un blocage contourné en +silence se paie en heures** — la panne est alors cherchée au mauvais endroit. + +Le rôle `Administrator` sur le jeton Proxmox y est recommandé **et signalé comme tel**, +avec le minimum documenté en regard : mieux vaut un privilège large assumé et resserré +ensuite qu'un privilège serré qu'on élargit en panique au milieu d'un déploiement. + ## 2026-08-12 — La donnée revient : restauration éprouvée, pas seulement sauvegarde On savait que la donnée partait et arrivait. On ne savait pas qu'elle **revenait** — et diff --git a/README.md b/README.md index c7db645..f653387 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,12 @@ Le dépôt est le **moteur** (générique, partageable). Chaque déploiement ré ## Par où entrer — selon ce que tu viens faire -On n'arrive pas avec un *sujet*, on arrive avec une **situation**. Il y en a quatre : +On n'arrive pas avec un *sujet*, on arrive avec une **situation**. Il y en a cinq : | Ta situation | Ta porte | |---|---| | « Je veux **monter** mon écosystème sur ma grappe Proxmox » | [`QUICKSTART.md`](QUICKSTART.md) | +| « Je **prête mon matériel** à quelqu'un qui déploiera dessus » | [`docs/preparer-un-site-hebergeur.md`](docs/preparer-un-site-hebergeur.md) | | « Je viens d'**hériter** d'un écosystème déjà déployé, je dois l'exploiter » | [`wiki/Reprendre-l-écosystème.md`](wiki/Reprendre-l-%C3%A9cosyst%C3%A8me.md) | | « Je dois **modifier** le moteur » | [`docs/carte-set-ops.md`](docs/carte-set-ops.md) | | « J'**apprends** le métier » | [`wiki/Home.md`](wiki/Home.md) | diff --git a/docs/audit/preuve-2026-08-12.md b/docs/audit/preuve-2026-08-12.md index cbda2a5..527e052 100644 --- a/docs/audit/preuve-2026-08-12.md +++ b/docs/audit/preuve-2026-08-12.md @@ -46,7 +46,7 @@ | P31 | Documentation : tout ce que le depot FAIT est nomme | — | ✅ OK | 44 scripts expliques et atteignables, 92 cibles make documentees, 54 roles avec README. | | P32 | Intrants exiges par les roles : tous fournis | — | ✅ OK | CONFORME : 35 exigence(s) de role, toutes satisfaites (126 cle(s) declaree(s) par l'instance). | | P33 | Aucune collision de port entre roles co-localises | — | ✅ OK | CONFORME : 32 revendication(s) de port, aucune collision entre roles co-localises (33 groupes). | -| P34 | Chaque document declare son lecteur | — | ✅ OK | 38 document(s) declarent leur lecteur (15 genere(s) exempte(s)). | +| P34 | Chaque document declare son lecteur | — | ✅ OK | 39 document(s) declarent leur lecteur (15 genere(s) exempte(s)). | | P35 | Toute application exigeant une base en a une au plan | — | ✅ OK | 5 application(s) exigeant une base l'ont toutes (4 entree(s) au registre). | | P36 | Tout detenteur d'etat porte une sauvegarde | — | ✅ OK | 9 hote(s) detiennent de l'etat, tous porteurs de `client_backup` (9 groupe(s) au catalogue). | diff --git a/docs/preparer-un-site-hebergeur.md b/docs/preparer-un-site-hebergeur.md new file mode 100644 index 0000000..94452fa --- /dev/null +++ b/docs/preparer-un-site-hebergeur.md @@ -0,0 +1,169 @@ +# Préparer un site hébergeur + +> **Pour qui :** l'**hébergeur** qui met son matériel à disposition — il n'a pas besoin de +> connaître Set-OPS. L'exploitant du tenant lui transmet ce document, puis relit §7 pour +> savoir ce qu'il doit recevoir en retour. + +Set-OPS déploie un écosystème complet (annuaire, SSO, courriel, forge, nuage +collaboratif, supervision, sauvegardes) à partir d'un plan. Tout cet adressage se +**dérive** d'un seul chiffre — l'`index` du tenant. + +Ce que la machine ne peut pas deviner, c'est le **matériel** : le câblage, le nom des +stockages, le chemin de sortie vers Internet. C'est l'objet de ce document, et c'est tout +ce qui est demandé à l'hébergeur. + +## 1. Le partage des rôles + +| | Décide quoi | Où ça vit | +|---|---|---| +| **Hébergeur** | réseau physique, pare-feu de bordure, stockage, hyperviseur | `underlay.yml` et `proxmox-hebergeur.yml`, dans **son** dépôt | +| **Tenant** | quels services, sur quel nœud/stockage/pont les poser | `inventories/*/group_vars/`, dans le dépôt du tenant | + +Cette séparation n'est pas cosmétique. Tant que ces valeurs étaient recopiées chez chaque +tenant, elles ont **divergé** : deux inventaires contradictoires du même cluster, avec des +listes de stockages et de ponts différentes. Un hébergeur décrit son matériel **une fois**. + +## 2. Le plan d'adressage — la seule chose à respecter à la lettre + +| Rôle | VLAN | Sous-réseau | MTU | Qui y vit | +|---|---|---|---|---| +| Gestion | 10 | `10.0.0.0/24` | 1500 | frontière `.1`, commutateurs, OOB/IPMI, admin hyperviseur | +| Transport VXLAN | 11 | `10.0.5.0/24` | 1500 | hyperviseurs seulement | +| Stockage iSCSI | 20 | `10.0.1.0/24` | 9000 | baie ↔ hyperviseurs | +| Sortie des tenants | 40 | `10.0.4.0/24` | 1500 | frontière `.1`, nœuds de sortie | + +Deux invariants portés par la preuve `P23` : + +- **Un point de routage porte le même dernier octet partout** : `.1`. La passerelle d'un + sous-réseau garde cette adresse, quel que soit l'équipement qui l'assure. +- **Chaque tenant occupe `10.(10+index).0.0/16`.** Index 11 → `10.21.0.0/16`, index 17 → + `10.27.0.0/16`. Aucun réseau de l'hébergeur ne doit chevaucher ces blocs : le symptôme + d'une collision est un service qui « ne répond pas » sans aucune erreur nulle part. + +### Le MTU n'est pas un détail + +Le trafic des VM voyage encapsulé en VXLAN, ce qui coûte **50 octets**. Avec un transport +à 1500, les VM tournent à **1450** — automatiquement, mais à condition que 1500 passe +réellement de bout en bout sur les VLAN 11 et 40. Un MTU rogné en chemin donne le pire des +symptômes : les petites requêtes passent, les grosses meurent, et rien n'est signalé. + +Le jumbo (9000) sur le stockage est facultatif. **À moitié configuré, il ne fonctionne +pas du tout** — mieux vaut rester à 1500 que le poser sur un seul des trois maillons. + +## 3. La frontière (pare-feu de bordure) + +C'est le seul équipement qui route, et l'unique point de passage entre l'extérieur et +l'intérieur. Set-OPS **pilote ses règles par API** : le boîtier reste à l'hébergeur, rien +n'y est installé. + +**Interfaces :** le WAN (noter l'IP publique) ; la gestion en `10.0.0.1/24` ; le transit +en `10.0.4.1/24`, par où sortent les machines du tenant. + +**Un compte pour l'exploitant :** un utilisateur `ansible` avec sa clé publique. +**Aucun `sudo` n'est nécessaire** — l'outil lit et appelle l'API. Un compte qui ne peut +pas devenir root ne peut pas casser le pare-feu par accident. + +**Une clé d'API :** sur OPNsense, *System → Access → Users → `ansible` → API keys → « + »*. +Le **secret n'est affiché qu'une seule fois**. + +## 4. Le stockage + +Rien de spécifique à Set-OPS : il faut que l'hyperviseur puisse y poser des disques de VM +(NFS ou iSCSI). Seuls comptent les stockages qui acceptent le contenu **`images`**. + +**Dimensionnement**, mesuré sur un écosystème complet de quatorze machines : + +| | Mesuré | À prévoir | +|---|---|---| +| Disques provisionnés | ~460 Go | 600 Go | +| RAM allouée | ~37 Go | 48 Go (32 à la rigueur, en démarrant par vagues) | + +## 5. L'hyperviseur (Proxmox VE) + +**À noter et transmettre :** le **nom exact du nœud** tel qu'il apparaît dans l'interface, +les **noms exacts** des stockages, et les **ponts** (`vmbrN`). Pas les descriptions : les +noms, tels qu'ils seront lus par l'API. + +Sur un cluster de plusieurs nœuds, un pont doit exister **sur tous** — un pont partiel est +un piège : la VM ne démarre que sur certains nœuds, et l'erreur ne le dit pas. + +**Le SDN n'est pas à configurer par l'hébergeur.** Set-OPS crée le contrôleur EVPN, la +zone (un VRF par tenant) et les réseaux. Il faut seulement que le SDN soit disponible et +le VLAN de transport en place. + +### Le compte d'API + +``` +pveum user add ansible@pve +pveum aclmod / -user ansible@pve -role Administrator +pveum user token add ansible@pve set-ops --privsep 0 +``` + +Le **Token ID** et le **secret** sont à noter — le secret n'est affiché qu'une fois. + +> **Sur `Administrator`, autant le dire franchement.** Le minimum documenté est +> `VM.Allocate`, `VM.Clone`, `VM.Config.*`, `Datastore.AllocateSpace` et les droits SDN. +> Mais l'outil crée aussi des objets réseau et détruit des VM : partir large le premier +> jour évite de courir après des `403` pendant le déploiement. **Le resserrer ensuite est +> un rôle sur mesure, dix minutes** — et c'est un geste à faire, pas une intention. + +## 6. Le gabarit — et le piège qu'il porte + +Toutes les VM sont clonées depuis un même modèle Debian, nommé **`modeleSetOPS`** : c'est +un artefact du **moteur**, pas d'un tenant. Une VM Debian minimale avec +`qemu-guest-agent` et `cloud-init`, convertie en template, suffit. + +> **Ne pas le personnaliser.** Un gabarit qui porte une clé privée d'hôte SSH, un +> `/etc/resolv.conf` figé ou un compte nominatif recopie tout cela dans **chaque** clone. +> C'est arrivé, et il a fallu recapturer le gabarit puis reconstruire pour s'en défaire. + +En cas de doute, laisser l'exploitant le fabriquer : c'est une demi-heure, et la procédure +est écrite (`docs/procedure-template-debian13-proxmox.md`). + +## 7. Ce que l'exploitant doit recevoir + +| Élément | Exemple | +|---|---| +| IP publique (WAN) | `203.0.113.10` | +| Nom du nœud hyperviseur | `asgard` | +| Stockages (noms exacts) | `TrueNAS`, `local-lvm` | +| Ponts | `vmbr0`, `vmbr3` | +| RAM et disque disponibles | 64 Go / 2 To | +| Modèle du commutateur | dicte le dialecte de CLI (`cisco` \| `binardat`) | +| Domaine public prévu | `exemple.ca` | +| Gabarit | fait, ou à faire | +| Accès | sur place, ou VPN débouchant sur le VLAN 10 | + +**Et deux paires de secrets**, par un canal chiffré et séparément du reste : le Token ID + +secret de l'hyperviseur, la clé + secret de l'API de la frontière. Ils n'entrent **jamais** +dans un dépôt git : leur place est la voûte chiffrée de l'instance. + +### Les accès réseau nécessaires + +| Cible | Port | Pour quoi | +|---|---|---| +| Hyperviseur | 8006 | créer et détruire les VM | +| Frontière | 443 | poser les règles par API | +| Frontière | 22 | SSH, compte `ansible` | +| Réseau du tenant | 22 | SSH vers les VM une fois créées | + +Les VM ont besoin de sortir sur Internet pour leurs paquets système. Le reste — les +artefacts applicatifs — est **poussé depuis le poste de l'exploitant**, pas téléchargé par +les machines. + +## 8. Ce qu'il ne faut pas faire + +- **Ne pas créer de VM à la main** pour le tenant. Elles sont toutes dérivées du plan ; + une VM créée à côté est invisible pour l'outil, qui la détruira sans le savoir. +- **Ne pas configurer le SDN** : l'outil le fait entièrement. +- **Ne pas écrire les secrets** dans un fichier partagé ni dans un dépôt git. +- **Ne pas contourner un blocage en silence.** Un point resté ouvert et *signalé* se règle + en dix minutes ; un contournement non dit se paie en heures, parce que la panne sera + cherchée au mauvais endroit. + +## Voir aussi + +- [`sdn-evpn.md`](sdn-evpn.md) — pourquoi une zone EVPN par tenant. +- [`config-proxmox.md`](config-proxmox.md) — les intrants côté hyperviseur, et le token. +- [`frontiere-opnsense.md`](frontiere-opnsense.md) — la frontière, hors flotte Ansible. +- [`procedure-template-debian13-proxmox.md`](procedure-template-debian13-proxmox.md) — fabriquer `modeleSetOPS`. diff --git a/scripts/config_proxmox.py b/scripts/config_proxmox.py index 140d442..84b41a6 100644 --- a/scripts/config_proxmox.py +++ b/scripts/config_proxmox.py @@ -53,7 +53,7 @@ VALEURS_DEFAUT = { "proxmox_validate_certs": False, "proxmox_clone_noeud": "", "proxmox_clone_vmid_modele": 9000, - "proxmox_clone_source_nom": "modele-debian13", + "proxmox_clone_source_nom": "modeleSetOPS", "proxmox_clone_stockage": "", "proxmox_clone_format": "", "proxmox_clone_complet": True, diff --git a/scripts/model_creer.py b/scripts/model_creer.py index a08ea4c..ecae467 100644 --- a/scripts/model_creer.py +++ b/scripts/model_creer.py @@ -163,7 +163,7 @@ def _generaliser(modele: Path, source: Path) -> None: for cle in PROXMOX_A_VIDER: _sub_champ(px_f, rf'(?m)^{cle}:.*$', f'{cle}: ""') _sub_champ(px_f, r'(?m)^proxmox_clone_source_nom:.*$', - 'proxmox_clone_source_nom: "modele-debian13"') + 'proxmox_clone_source_nom: "modeleSetOPS"') def creer(mode: str, nom: str, base: str | None, source: str | None, dest: str | None) -> Path: