modèles : l'underlay devient modélisable, et P17 le valide
Un modèle décrivait un tenant — ses services, ses zones, ses bases. Tous les hébergeurs n'ont pas le même matériel : l'infrastructure physique mérite le même traitement. Le modèle public gagne un underlay volontairement minimal (un commutateur, pas de fabric de stockage séparée), point de départ honnête d'un petit hébergeur. Les montages plus riches sont d'autres modèles, conformément à la doctrine : un générique public, les étoffés en privé. Le modèle contient désormais deux moitiés qui ne vont pas au même endroit : `plan/` et `inventories/` chez le tenant, `underlay.yml` chez l'hébergeur. `modeles.py verifier` le valide (P17), facultativement et sur sa cohérence INTERNE seulement — pas contre les tenants fédérés réels, un modèle étant un gabarit et non un site déployé. Il a fallu rendre paramétrables deux hypothèses du validateur, qui lisait la nomenclature de l'instance active et globait les dépôts frères ; comportement par défaut inchangé. Cinq cas de rejet exercés : VLAN empiétant sur la plage tenant, passerelle au mauvais dernier octet (lue dans la nomenclature du modèle), routeur inconnu, sortie hors du lien, port déclaré deux fois. Preuves : 24 OK, 0 échec. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
0eae97c6ec
commit
7256486c9c
7 changed files with 161 additions and 7 deletions
30
CHANGELOG.md
30
CHANGELOG.md
|
|
@ -1,5 +1,35 @@
|
|||
# CHANGELOG — Set-OPS
|
||||
|
||||
## 2026-08-02 (suite 6) — l'underlay devient modélisable
|
||||
|
||||
Un modèle décrivait jusqu'ici un **tenant** : ses services, ses zones, ses bases. Or tous les
|
||||
hébergeurs n'ont pas le même matériel, et l'infrastructure physique mérite le même traitement.
|
||||
|
||||
### Ajouté — `exemples/modeles/socle/underlay.yml`
|
||||
Le modèle public gagne un underlay **volontairement minimal** : un seul commutateur, pas de
|
||||
fabric de stockage séparée. C'est le point de départ honnête d'un petit hébergeur ; les
|
||||
montages plus riches (étoile à trois commutateurs, paire en MLAG, stockage jumbo dédié) sont
|
||||
d'autres modèles, conformément à la doctrine — un générique public, les étoffés en privé.
|
||||
|
||||
Le modèle contient désormais **deux moitiés qui ne vont pas au même endroit** : `plan/` et
|
||||
`inventories/` chez le tenant, `underlay.yml` chez l'hébergeur. Chez un hébergeur qui est son
|
||||
propre tenant, les deux atterrissent au même dépôt — c'est le cas particulier, pas la règle.
|
||||
|
||||
### Étendu — `modeles.py verifier` valide l'underlay (preuve P17)
|
||||
La validation est **facultative** (un modèle sans underlay reste valide) et porte sur la
|
||||
cohérence **interne** seulement : VLAN sous la plage tenant, sous-réseaux disjoints,
|
||||
passerelle dans son réseau, routeur déclaré, ports non dupliqués, dernier octet partagé.
|
||||
|
||||
Elle n'est **pas** confrontée aux tenants fédérés réels : un modèle est un gabarit, pas un
|
||||
site déployé. Il a fallu pour cela rendre paramétrables deux hypothèses du validateur, qui
|
||||
lisait la nomenclature de l'instance *active* et globait les dépôts frères — sur un modèle,
|
||||
les deux auraient été faux. `charger_depuis()` et `plan_nomenclature=` ; comportement par
|
||||
défaut inchangé.
|
||||
|
||||
Cinq cas de rejet exercés sur un modèle fautif : VLAN empiétant sur la plage tenant,
|
||||
passerelle au mauvais dernier octet (lue dans la nomenclature **du modèle**), routeur inconnu,
|
||||
sortie hors du lien, port déclaré deux fois.
|
||||
|
||||
## 2026-08-02 (suite 5) — l'underlay rejoint le dépôt de l'hébergeur
|
||||
|
||||
`underlay.yml` vivait **gitignoré** à la racine du moteur : consommé par deux générateurs,
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@
|
|||
| P14 | Pas de chemin lab/ code en dur | AFF-097 | ✅ OK | Aucun chemin instance/inventories/lab/group_vars code en dur. |
|
||||
| P15 | Modele public socle valide | AFF-022, AFF-099 | ✅ OK | Modele public socle : domaines/serveurs/applications/bases valides. |
|
||||
| P16 | Inventaire Ansible complet (--list) | AFF-030 | ✅ OK | 14 hotes, 32 groupes (inventaire dechiffre et parse). |
|
||||
| P17 | Tous les modeles d'instance valident | AFF-022, AFF-099 | ✅ OK | Les 1 modele(s) decouvert(s) valident. |
|
||||
| P17 | Tous les modeles valident (registres + underlay) | AFF-022, AFF-099 | ✅ OK | Les 1 modele(s) decouvert(s) valident. |
|
||||
| P18 | Gabarit de voute complet | AFF-026 | ✅ OK | Gabarit de voute complet : 24 secret(s) exige(s), tous presents. |
|
||||
| P19 | Le GUI couvre le schema du plan | AFF-002, AFF-095 | ✅ OK | GUI : les 28 champ(s) des plans reels sont editables (2 plan(s) inspecte(s)), registres toleres : nomenclature. |
|
||||
| P20 | Adressage 100% derive du seed (aucun stocke) | AFF-001, AFF-003 | ✅ OK | 2 nomenclature(s) : adressage 100% derive du seed index. |
|
||||
|
|
|
|||
|
|
@ -6,3 +6,24 @@ vers un écosystème complet (voir modèle `integral`).
|
|||
|
||||
Zones : Frontière (VLAN 16) · Services-infra (VLAN 19). VMID `ip-miroir` 9 chiffres.
|
||||
Copier vers `OPS-<tenant>`, adapter `index`/domaine/intrants, puis `make instancier`.
|
||||
|
||||
## Deux moitiés, deux propriétaires
|
||||
|
||||
Ce modèle en contient **deux**, qui ne vont pas au même endroit :
|
||||
|
||||
| Fichiers | Décrit | Copier vers |
|
||||
|---|---|---|
|
||||
| `plan/`, `inventories/` | les **services** d'une organisation | `OPS-<tenant>` |
|
||||
| `underlay.yml` | le **matériel** qui les porte | `OPS-<hébergeur>` |
|
||||
|
||||
Un tenant qui se fait héberger prend la première moitié seulement : l'underlay appartient à
|
||||
qui possède les commutateurs. Chez un hébergeur qui est aussi son propre tenant, les deux
|
||||
atterrissent dans le même dépôt — c'est un cas particulier, pas la règle.
|
||||
|
||||
L'underlay fourni est **volontairement minimal** : un seul commutateur, pas de fabric de
|
||||
stockage séparée. Tous les hébergeurs n'ont pas le même matériel ; les montages plus riches
|
||||
(étoile à trois commutateurs, paire en MLAG, stockage jumbo dédié) relèvent d'autres modèles.
|
||||
|
||||
Il est **facultatif** : un modèle sans `underlay.yml` reste valide. Présent, il est vérifié
|
||||
pour sa cohérence interne par la preuve **P17**, sans être confronté aux tenants réels — un
|
||||
modèle est un gabarit, pas un site déployé.
|
||||
|
|
|
|||
67
exemples/modeles/socle/underlay.yml
Normal file
67
exemples/modeles/socle/underlay.yml
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
# Underlay du modèle « socle » — l'infrastructure PHYSIQUE la plus simple qui tienne.
|
||||
#
|
||||
# Un modèle d'underlay décrit le matériel d'un HÉBERGEUR, pas un plan de services :
|
||||
# tous les hébergeurs n'ont pas les mêmes équipements. Celui-ci est volontairement
|
||||
# minimal — un seul commutateur, pas de fabric de stockage séparée — parce que c'est
|
||||
# le point de départ honnête d'un petit hébergeur. Les montages plus riches (étoile à
|
||||
# trois commutateurs, paire en MLAG, stockage jumbo dédié) sont d'autres modèles.
|
||||
#
|
||||
# À l'usage : copier ce fichier dans le dépôt de l'hébergeur, l'adapter, puis le monter
|
||||
# ln -s ../OPS-<hebergeur>/underlay.yml underlay.yml
|
||||
# Validé par `python3 scripts/modeles.py verifier` (preuve P17, structure du modèle)
|
||||
# puis, une fois adapté et monté, par `make underlay` (site réel, preuve P23).
|
||||
---
|
||||
underlay:
|
||||
# LE commutateur, qui route tout. Avec un seul équipement il n'y a pas de switches
|
||||
# d'accès : le devis n'émet donc que sa partie A.
|
||||
routeur: switch-01
|
||||
|
||||
# Dialecte de CLI : décide la forme des masques d'ACL, des routes, du spanning-tree.
|
||||
dialecte: cisco
|
||||
|
||||
# Un seul commutateur = aucun lien redondant, donc aucune boucle par construction.
|
||||
# RSTP reste actif comme filet : deux ports brassés ensemble par mégarde suffiraient
|
||||
# à provoquer une tempête de diffusion.
|
||||
stp:
|
||||
mode: rstp
|
||||
topologie: etoile
|
||||
|
||||
reseaux:
|
||||
# Plan de gestion : le commutateur lui-même, l'hyperviseur, l'OOB/IPMI.
|
||||
- nom: management
|
||||
description: Commutateur, mgmt de l'hyperviseur, OOB/IPMI
|
||||
vlan: 10
|
||||
sous_reseau: 10.0.0.0/24
|
||||
passerelle: 10.0.0.1
|
||||
mtu: 1500
|
||||
|
||||
# Lien vers le pare-feu de bordure. SANS LUI, la flotte n'a ni sortie vers
|
||||
# Internet ni chemin de retour vers l'administration : elle serait routée
|
||||
# jusqu'à la bordure puis muette, ce qui est très difficile à diagnostiquer.
|
||||
# `passerelle_sortie` = l'adresse du pare-feu sur ce lien ; c'est elle qui fait
|
||||
# émettre la route par défaut et les routes de retour.
|
||||
- nom: transit-frontiere
|
||||
description: Lien commutateur L3 <-> pare-feu de bordure
|
||||
vlan: 40
|
||||
sous_reseau: 10.0.4.0/29
|
||||
# Le /29 loge deux pare-feux (transition, ou haute disponibilité plus tard) :
|
||||
# ils occupent le bas de la plage, le SVI du commutateur le haut.
|
||||
passerelle: 10.0.4.6 # SVI du commutateur
|
||||
passerelle_sortie: 10.0.4.1 # pare-feu = sortie par défaut de la flotte
|
||||
mtu: 1500
|
||||
|
||||
# Équipements fixes. Le routeur PORTE le SVI de management : son adresse de gestion
|
||||
# EST la passerelle du réseau — `make underlay` refuse deux valeurs divergentes.
|
||||
hotes:
|
||||
- nom: switch-01
|
||||
reseau: management
|
||||
ip: 10.0.0.1
|
||||
# Ports physiques (optionnel). Déclarés, le devis sort applicable tel quel ;
|
||||
# absents, il émet des marqueurs `<PORT-VERS-...>` à remplacer à la main — et
|
||||
# le travail est perdu à chaque régénération.
|
||||
ports:
|
||||
hyperviseurs: [Gi1/0/1]
|
||||
frontiere: [Gi1/0/23]
|
||||
# Le pare-feu de bordure : hors flotte Ansible, déclaré ici pour documenter le
|
||||
# lien et réserver son nom.
|
||||
- { nom: pare-feu-1, reseau: transit-frontiere, ip: 10.0.4.1 }
|
||||
|
|
@ -31,6 +31,7 @@ import sys
|
|||
from pathlib import Path
|
||||
|
||||
RACINE = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(RACINE / "scripts"))
|
||||
MODELES_DEPOT = RACINE / "exemples" / "modeles"
|
||||
VALIDATEURS = ("domaines", "serveurs", "applications", "bases_donnees")
|
||||
|
||||
|
|
@ -63,8 +64,33 @@ def decouvrir() -> list[Path]:
|
|||
return list(vus.values())
|
||||
|
||||
|
||||
def verifier_underlay(modele: Path) -> list[str]:
|
||||
"""Valide l'underlay du modele, s'il en porte un. Liste vide sinon.
|
||||
|
||||
Un modele d'underlay decrit le MATERIEL d'un hebergeur : tous n'ont pas les memes
|
||||
equipements, d'ou l'interet d'en avoir plusieurs. On le valide pour sa coherence
|
||||
INTERNE (VLAN sous la plage tenant, sous-reseaux disjoints, passerelle dans son
|
||||
reseau, routeur declare, ports non dupliques, dernier octet partage) — mais PAS
|
||||
contre les tenants federes reels : un modele est un gabarit, pas un site deploye.
|
||||
D'ou les listes de tenants vides, et la nomenclature du modele plutot que celle de
|
||||
l'instance active.
|
||||
"""
|
||||
fichier = modele / "underlay.yml"
|
||||
if not fichier.is_file():
|
||||
return []
|
||||
import underlay as underlay_mod
|
||||
u = underlay_mod.charger_depuis(fichier)
|
||||
if u is None:
|
||||
return ["underlay: fichier present mais bloc `underlay:` absent ou vide"]
|
||||
erreurs = underlay_mod.valider(
|
||||
u, tenants_vlans=set(), tenants_supernets=[],
|
||||
plan_nomenclature=modele / "plan" / "nomenclature.yml",
|
||||
)
|
||||
return [f"underlay: {e}" for e in erreurs]
|
||||
|
||||
|
||||
def verifier_un(modele: Path) -> list[str]:
|
||||
"""Rejoue les 4 validateurs sur un modele. Renvoie la liste des erreurs."""
|
||||
"""Rejoue les validateurs de registres sur un modele, + son underlay s'il en a un."""
|
||||
erreurs: list[str] = []
|
||||
env = {**os.environ, "SETOPS_INSTANCE": str(modele)}
|
||||
env.pop("SETOPS_INVENTAIRE", None)
|
||||
|
|
@ -74,6 +100,7 @@ def verifier_un(modele: Path) -> list[str]:
|
|||
if r.returncode != 0:
|
||||
lignes = (r.stdout + r.stderr).strip().splitlines()
|
||||
erreurs.append(f"{nom}: {lignes[-1] if lignes else 'rc!=0'}")
|
||||
erreurs += verifier_underlay(modele)
|
||||
return erreurs
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -275,7 +275,7 @@ PREUVES: list[dict] = [
|
|||
"func": preuve_pas_lab_code_en_dur},
|
||||
{"id": "P15", "titre": "Modele public socle valide", "refs": ["AFF-022", "AFF-099"],
|
||||
"func": preuve_modele_socle},
|
||||
{"id": "P17", "titre": "Tous les modeles d'instance valident",
|
||||
{"id": "P17", "titre": "Tous les modeles valident (registres + underlay)",
|
||||
"refs": ["AFF-022", "AFF-099"],
|
||||
"cmds": [[sys.executable, "scripts/modeles.py", "verifier"]]},
|
||||
{"id": "P18", "titre": "Gabarit de voute complet", "refs": ["AFF-026"],
|
||||
|
|
|
|||
|
|
@ -181,14 +181,22 @@ def reseau_transit(underlay: dict | None) -> dict | None:
|
|||
return None
|
||||
|
||||
|
||||
def octet_passerelle() -> int | None:
|
||||
def charger_depuis(chemin_fichier: Path) -> dict | None:
|
||||
"""Le bloc `underlay` d'un fichier DESIGNE — pour valider un modele, pas le site."""
|
||||
if not chemin_fichier.is_file():
|
||||
return None
|
||||
data = yaml.safe_load(chemin_fichier.read_text(encoding="utf-8")) or {}
|
||||
return data.get("underlay") or None
|
||||
|
||||
|
||||
def octet_passerelle(plan_nomenclature: Path | None = None) -> int | None:
|
||||
"""Dernier octet reserve aux points de routage, lu dans la nomenclature active.
|
||||
|
||||
Convention d'exploitation : un routeur porte LE MEME dernier octet sur tous les
|
||||
sous-reseaux ou il participe — on retient une adresse, pas treize. Declare une
|
||||
seule fois (`reservations.passerelle`), jamais code en dur ici.
|
||||
"""
|
||||
p = RACINE / "instance" / "plan" / "nomenclature.yml"
|
||||
p = plan_nomenclature or (RACINE / "instance" / "plan" / "nomenclature.yml")
|
||||
if not p.is_file():
|
||||
return None
|
||||
n = yaml.safe_load(p.read_text(encoding="utf-8")) or {}
|
||||
|
|
@ -207,7 +215,8 @@ def _tenants_reperes() -> tuple[set[int], list[str]]:
|
|||
|
||||
def valider(underlay: dict | None,
|
||||
tenants_vlans: set[int] | None = None,
|
||||
tenants_supernets: list[str] | None = None) -> list[str]:
|
||||
tenants_supernets: list[str] | None = None,
|
||||
plan_nomenclature: Path | None = None) -> list[str]:
|
||||
"""Liste des erreurs (vide = conforme). Verifie champs requis, unicite, et
|
||||
surtout l'absence de collision avec la plage tenant (VLAN et sous-reseaux)."""
|
||||
if underlay is None:
|
||||
|
|
@ -291,7 +300,7 @@ def valider(underlay: dict | None,
|
|||
# Invariant du dernier octet : tous les points de routage doivent le partager.
|
||||
# Exemption des sous-reseaux plus etroits qu'un /24 : sur un /29 de transit,
|
||||
# l'adressage est dicte par les participants du lien, pas par la convention.
|
||||
attendu = octet_passerelle()
|
||||
attendu = octet_passerelle(plan_nomenclature)
|
||||
if attendu is not None:
|
||||
for r in reseaux(underlay):
|
||||
gw = r.get("passerelle")
|
||||
|
|
|
|||
Loading…
Reference in a new issue