P23 : la bande basse de D-77 devient une regle outillee
D-77 disait ou l'underlay doit vivre ; rien ne le verifiait. Une convention qu'on n'outille pas pourrit en silence (D-70) — c'est ce qui a laisse la sauvegarde vide pendant un mois. Le controle disait l'INVERSE de la decision : underlay.py refusait tout chevauchement avec un supernet tenant. Rendu plus FIN, pas plus strict : son propre supernet, bande basse -> conforme (la regle) son propre supernet, bande haute -> REFUS (collision avec ses zones) supernet d'un AUTRE site -> REFUS (jamais reliables) hors de tout supernet -> conforme (heritage 10.0.x, stockage 192.168.x) La frontiere derive d'OCTET_ZONE, jamais ecrite en dur. Le site declare son `index` dans underlay.yml ; sans lui on retombe sur la regle stricte d'avant D-77, sure — Chezlepro reste donc conforme en 10.0.x tant qu'il n'a pas migre. LE PIEGE, attrape par le test : un prefixe peut COMMENCER dans la bande basse et deborder — 10.21.0.0/19 couvre les octets 0 a 31. La borne est evaluee sur toute l'etendue du prefixe, pas sur son premier octet. Deux de mes propres cas d'epreuve etaient mal choisis (10.21.14.0/23 et 10.21.12.0/21 se normalisent dans la bande basse) : il a fallu un prefixe qui franchit reellement la frontiere pour eprouver la garde. test_underlay_bande_basse.py : 7 cas, cable dans make test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
0c93569dd7
commit
b7239149bb
5 changed files with 180 additions and 8 deletions
34
CHANGELOG.md
34
CHANGELOG.md
|
|
@ -1,5 +1,39 @@
|
|||
# CHANGELOG — Set-OPS
|
||||
|
||||
## 2026-08-12 — P23 outille D-77 : la bande basse devient une règle, pas une convention
|
||||
|
||||
D-77 disait où l'underlay doit vivre. Rien ne le vérifiait — et une convention qu'on
|
||||
n'outille pas pourrit en silence (D-70). C'est exactement ce qui a laissé la sauvegarde
|
||||
vide pendant un mois.
|
||||
|
||||
**Le contrôle disait l'inverse de la décision.** `underlay.py` refusait *tout*
|
||||
chevauchement avec un supernet tenant. Il fallait le rendre plus **fin**, pas plus strict :
|
||||
|
||||
| Situation | Verdict |
|
||||
|---|---|
|
||||
| dans **son propre** supernet, bande basse | conforme — c'est la règle |
|
||||
| dans son propre supernet, bande **haute** | **refusé** — collision avec ses propres zones |
|
||||
| dans le supernet d'un **autre** site | **refusé** — les deux ne pourront jamais être reliés |
|
||||
| hors de tout supernet (`10.0.x`, `192.168.x`) | conforme — héritage, et stockage |
|
||||
|
||||
La frontière est **dérivée** de `OCTET_ZONE`, jamais écrite en dur : déplacer la règle des
|
||||
zones déplace la borne avec elle. Le site déclare son `index` dans `underlay.yml` ; sans
|
||||
lui, on retombe sur la règle stricte d'avant D-77 — le comportement sûr pour un underlay
|
||||
qui n'a pas encore migré. **Chezlepro reste donc conforme aujourd'hui**, en `10.0.x`.
|
||||
|
||||
### Le piège que le test attrape
|
||||
|
||||
Un préfixe peut **commencer** dans la bande basse et déborder : `10.21.0.0/19` couvre les
|
||||
octets 0 à 31. Une vérification qui ne regarderait que le premier octet le laisserait
|
||||
passer. La borne est donc évaluée sur **toute l'étendue** du préfixe.
|
||||
|
||||
Deux de mes propres cas d'épreuve étaient mal choisis — `10.21.14.0/23` et `10.21.12.0/21`
|
||||
se normalisent entièrement dans la bande basse, et « conforme » y était la bonne réponse.
|
||||
Il a fallu construire un préfixe qui franchit réellement la frontière pour éprouver la
|
||||
garde.
|
||||
|
||||
`scripts/tests/test_underlay_bande_basse.py` — 7 cas, câblé dans `make test`.
|
||||
|
||||
## 2026-08-12 — `modeleSetOPS`, et une porte pour l'hébergeur
|
||||
|
||||
### Le gabarit portait le nom du mauvais propriétaire
|
||||
|
|
|
|||
1
Makefile
1
Makefile
|
|
@ -211,6 +211,7 @@ test: ## Lance les tests unitaires (derivation de nomenclature et d'inventaire)
|
|||
python3 scripts/tests/test_inventory_host.py
|
||||
python3 scripts/tests/test_raser.py
|
||||
python3 scripts/tests/test_raser_resultat.py
|
||||
python3 scripts/tests/test_underlay_bande_basse.py
|
||||
|
||||
.PHONY: verifier
|
||||
verifier: lint test inventaire-verifier site-verifier flux-verifier syntaxe ## Rejoue les preuves SANS reecrire le rapport (verification rapide)
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ sont les seules vérifiables.
|
|||
| **D-71** | **Une PKI et un DNS fonctionnels avant toute chose** ; puis, par VM : socle → enrôlement PKI → enregistrement DNS (A **et** PTR) | `deployer-tout` déroule par COUCHES — correct, mais chaque VM réclame alors un certificat à une autorité pas encore debout, et l'échec se lit comme un défaut du rôle et non d'ordre. Les deux hôtes d'amorçage se **dérivent** de `applications.<step_ca\|powerdns>.hote` : déplacer l'autorité déplace l'amorçage. **Deux exceptions structurelles assumées** — l'AC s'auto-signe, le DNS pose son propre enregistrement | `Makefile` `_amorcer-socle`, `scripts/socle_amorcage.py` | — |
|
||||
| **D-76** | Le **gabarit doit se FABRIQUER par le dépôt**, pas se façonner à la main — décidée le 2026-08-12, **volontairement différée** après Technolibre | tout dérive d'un plan et se prouve ; le gabarit est la **seule pièce faite à la main** — 853 lignes de procédure manuelle — et il est en amont des quatorze VM. Le 2026-08-09 l'a démontré : l'ancien portait une **clé privée d'hôte SSH** et un `/etc/resolv.conf` figé, recopiés dans chaque clone, et il a fallu le recapturer. La preuve de reconstruction s'arrête donc un cran trop tôt : on reconstruit la flotte, pas ce dont elle est clonée. **Cible** : image cloud Debian officielle → signature vérifiée contre une empreinte épinglée (`verifier_signature.py`, déjà écrit) → import, réglages, conversion. **Coût honnête** : la recette doit être COMPARÉE au gabarit courant avant bascule — un réglage oublié serait hérité par les quatorze VM, et découvert loin de sa cause. **En attendant**, un gabarit se transporte d'un cluster à l'autre par `vzdump` / `qmrestore` | `docs/procedure-template-debian13-proxmox.md`, `scripts/verifier_signature.py` | — *(à construire)* |
|
||||
|
||||
| **D-77** | **L'underlay d'un site dérive du même `index` que son tenant**, dans la **bande basse** `10.(10+index).0–15.x` ; les réseaux de **stockage** en sortent et sont **identiques partout**, en `192.168.<vlan>.0/24` | tant qu'il n'existait qu'un site, `10.0.x.x` suffisait. Deux sites qui doivent se joindre — reprise mutuelle, sauvegardes croisées, exploitation à distance — **ne peuvent pas** porter les mêmes plages : chaque routeur croit que le réseau est chez lui. Un « numéro de site » a d'abord été proposé : **rejeté**, c'était un SECOND SEED à tenir et à synchroniser, alors que tout dérive déjà d'`index` (D-17). La bande basse est sûre **par la règle, pas par chance** : les zones valent `10.(10+index).(15+categorie).0/24`, donc troisième octet ≥ 16 — les octets 0 à 15 ne sont jamais alloués. Les troisièmes et derniers octets de l'underlay actuel sont ainsi **préservés** : seul le deuxième change, et l'invariant `.1` (D-04) survit intact. Le **stockage** en est sorti parce qu'il est jumbo, non routé, et ne quitte jamais son site : il n'a aucun besoin d'être unique, et l'aligner sur le VLAN (`192.168.20.x` ↔ VLAN 20) fait dire son VLAN à l'adresse. Contrepartie assumée : ces réseaux ne pourront jamais traverser un lien inter-sites — sans conséquence, puisque ce qui voyage entre deux sites est l'**état**, par le dépôt de sauvegarde, pas le stockage bloc | `underlay.yml`, `docs/preparer-un-site-hebergeur.md`, `scripts/inventory_rules.py` (`OCTET_ZONE`) | **P23** *(à étendre : borner le 3ᵉ octet sous `OCTET_ZONE + 1`)* |
|
||||
| **D-77** | **L'underlay d'un site dérive du même `index` que son tenant**, dans la **bande basse** `10.(10+index).0–15.x` ; les réseaux de **stockage** en sortent et sont **identiques partout**, en `192.168.<vlan>.0/24` | tant qu'il n'existait qu'un site, `10.0.x.x` suffisait. Deux sites qui doivent se joindre — reprise mutuelle, sauvegardes croisées, exploitation à distance — **ne peuvent pas** porter les mêmes plages : chaque routeur croit que le réseau est chez lui. Un « numéro de site » a d'abord été proposé : **rejeté**, c'était un SECOND SEED à tenir et à synchroniser, alors que tout dérive déjà d'`index` (D-17). La bande basse est sûre **par la règle, pas par chance** : les zones valent `10.(10+index).(15+categorie).0/24`, donc troisième octet ≥ 16 — les octets 0 à 15 ne sont jamais alloués. Les troisièmes et derniers octets de l'underlay actuel sont ainsi **préservés** : seul le deuxième change, et l'invariant `.1` (D-04) survit intact. Le **stockage** en est sorti parce qu'il est jumbo, non routé, et ne quitte jamais son site : il n'a aucun besoin d'être unique, et l'aligner sur le VLAN (`192.168.20.x` ↔ VLAN 20) fait dire son VLAN à l'adresse. Contrepartie assumée : ces réseaux ne pourront jamais traverser un lien inter-sites — sans conséquence, puisque ce qui voyage entre deux sites est l'**état**, par le dépôt de sauvegarde, pas le stockage bloc | `underlay.yml` (`index`), `scripts/underlay.py`, `docs/preparer-un-site-hebergeur.md` | **P23** |
|
||||
|
||||
> **Ce que D-77 rend possible, et ce qu'elle coûte.** Elle est la condition de la reprise
|
||||
> mutuelle : sans plages distinctes, aucun lien entre deux sites ne peut router. Elle est
|
||||
|
|
|
|||
82
scripts/tests/test_underlay_bande_basse.py
Normal file
82
scripts/tests/test_underlay_bande_basse.py
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
#!/usr/bin/env python3
|
||||
"""D-77 — l'underlay tient dans la BANDE BASSE du supernet de son propre site.
|
||||
|
||||
Ce que ce test garde, et pourquoi il existe. La regle « underlay dans la bande basse »
|
||||
n'est sure que parce que les zones d'un tenant commencent a `OCTET_ZONE + 1`. C'est une
|
||||
propriete de `inventory_rules`, pas une place qu'on aurait constatee libre un jour : si
|
||||
`OCTET_ZONE` bougeait sans que la borne suive, la validation continuerait a dire « oui »
|
||||
en laissant l'underlay recouvrir les zones — et personne ne le verrait avant que deux
|
||||
machines ne se disputent une adresse.
|
||||
|
||||
Le test derive donc sa frontiere de la MEME constante que le code, et verifie les trois
|
||||
cas qui comptent, y compris ceux ou un prefixe FRANCHIT la frontiere (un /19 couvre les
|
||||
octets 0 a 31 : il deborde, meme s'il commence dans la bande basse).
|
||||
"""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
RACINE = Path(__file__).resolve().parent.parent.parent
|
||||
sys.path.insert(0, str(RACINE / "scripts"))
|
||||
|
||||
import underlay as u # noqa: E402
|
||||
from inventory_rules import OCTET_ZONE # noqa: E402
|
||||
|
||||
SUPERNETS = ["10.21.0.0/16", "10.27.0.0/16"] # deux sites : index 11 et 17
|
||||
MIEN = 11
|
||||
|
||||
|
||||
def _erreurs(sous: str, index: int | None = MIEN) -> list[str]:
|
||||
site = {"reseaux": [{"nom": "g", "vlan": 10, "sous_reseau": sous}]}
|
||||
if index is not None:
|
||||
site["index"] = index
|
||||
return [e for e in u.valider(site, tenants_vlans=set(), tenants_supernets=SUPERNETS)
|
||||
if "deborde" in e or "chevauche le supernet" in e]
|
||||
|
||||
|
||||
def test_la_borne_derive_de_la_regle_des_zones():
|
||||
"""La frontiere n'est pas 16 en dur : elle suit `OCTET_ZONE`."""
|
||||
assert u.PREMIER_OCTET_ZONE == OCTET_ZONE + 1
|
||||
|
||||
|
||||
def test_bande_basse_de_son_propre_supernet_acceptee():
|
||||
for sous in ("10.21.0.0/24", "10.21.4.0/24", "10.21.5.0/24", "10.21.15.0/24"):
|
||||
assert _erreurs(sous) == [], f"{sous} devrait etre conforme (bande basse)"
|
||||
|
||||
|
||||
def test_bande_des_zones_refusee():
|
||||
for sous in (f"10.21.{OCTET_ZONE + 1}.0/24", "10.21.18.0/24", "10.21.16.0/20"):
|
||||
assert _erreurs(sous), f"{sous} devrait etre REFUSE (bande des zones)"
|
||||
|
||||
|
||||
def test_un_prefixe_qui_franchit_la_frontiere_est_refuse():
|
||||
"""Le piege : il COMMENCE dans la bande basse et deborde. Un test qui ne
|
||||
regarderait que le premier octet le laisserait passer."""
|
||||
for sous in ("10.21.0.0/19", "10.21.0.0/16"):
|
||||
assert _erreurs(sous), f"{sous} couvre au-dela de {OCTET_ZONE} : a refuser"
|
||||
|
||||
|
||||
def test_supernet_d_un_autre_site_refuse():
|
||||
"""C'est ce qui empeche deux sites d'etre relies — l'erreur la plus couteuse."""
|
||||
assert _erreurs("10.27.0.0/24"), "le supernet d'un autre site doit etre refuse"
|
||||
|
||||
|
||||
def test_hors_de_tout_supernet_accepte():
|
||||
"""Le stockage (192.168.x, identique partout) et l'heritage 10.0.x avant migration."""
|
||||
for sous in ("192.168.20.0/24", "192.168.31.0/24", "10.0.0.0/24"):
|
||||
assert _erreurs(sous) == [], f"{sous} est hors des supernets : conforme"
|
||||
|
||||
|
||||
def test_sans_index_on_retombe_sur_la_regle_stricte():
|
||||
"""Un underlay qui n'a pas encore migre ne declare pas d'index : aucun chevauchement
|
||||
n'est alors tolere, ce qui reste le comportement sur."""
|
||||
assert _erreurs("10.21.0.0/24", index=None), \
|
||||
"sans index, tout chevauchement doit etre refuse"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
n = 0
|
||||
for nom, fn in sorted(globals().items()):
|
||||
if nom.startswith("test_") and callable(fn):
|
||||
fn()
|
||||
n += 1
|
||||
print(f"OK — {n} test(s) D-77 (bande basse, frontiere derivee d'OCTET_ZONE={OCTET_ZONE}).")
|
||||
|
|
@ -1,13 +1,33 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Underlay = la fabric physique partagee par les instances federees.
|
||||
|
||||
Cluster-global, HORS de la derivation par-tenant (aucun index) : management des
|
||||
switches et du mgmt Proxmox/OOB, stockage iSCSI, reseaux Ceph (public + cluster).
|
||||
Ce sont des reseaux du *sous-sol* : ils portent la flotte, ils n'appartiennent a
|
||||
aucun tenant.
|
||||
Cluster-global : management des switches et du mgmt Proxmox/OOB, stockage iSCSI,
|
||||
reseaux Ceph (public + cluster). Ce sont des reseaux du *sous-sol* : ils portent la
|
||||
flotte, ils n'appartiennent a aucun tenant.
|
||||
|
||||
Convention : VLAN < 1000, franchement SOUS la plage tenant (VLAN = 1000+index*10+zone).
|
||||
Les sous-reseaux ne doivent chevaucher aucun supernet tenant (10.(10+index).0.0/16).
|
||||
|
||||
ADRESSAGE (D-77, 2026-08-12). Un site derive du meme `index` que son tenant, et son
|
||||
underlay occupe la BANDE BASSE de ce supernet : `10.(10+index).0-15.x`. La bande est
|
||||
sure PAR LA REGLE et non par chance — les zones d'un tenant valent
|
||||
`10.(10+index).(15+categorie).0/24`, donc troisieme octet >= 16 ; les octets 0 a 15
|
||||
ne sont jamais alloues. Deux sites ne peuvent alors pas se chevaucher, sans second
|
||||
registre a tenir.
|
||||
|
||||
Trois cas, et un seul est une erreur :
|
||||
|
||||
- dans SON PROPRE supernet, bande basse -> conforme (c'est la regle)
|
||||
- dans son propre supernet, bande HAUTE -> ERREUR : collision avec ses zones
|
||||
- dans le supernet d'un AUTRE tenant -> ERREUR : collision entre sites
|
||||
- hors de tout supernet (10.0.x, 192.168.x) -> conforme (heritage, et stockage)
|
||||
|
||||
Le site declare son index par `underlay.index`. **Sans lui**, on retombe sur la regle
|
||||
stricte d'avant D-77 (aucun chevauchement) : c'est le comportement sur pour un
|
||||
underlay qui n'a pas encore migre.
|
||||
|
||||
Les reseaux de STOCKAGE sortent de cet espace (192.168.<vlan>.0/24, identiques a tous
|
||||
les sites) : jumbo, non routes, ils ne quittent jamais leur site, donc ils n'ont aucun
|
||||
besoin d'etre uniques.
|
||||
|
||||
EMPLACEMENT : l'underlay appartient a l'HEBERGEUR — ses switches, ses cables. Il vit
|
||||
donc dans SON depot, et le moteur le monte par symlink :
|
||||
|
|
@ -36,6 +56,12 @@ from inventory_rules import supernet_de, vlan_de # noqa: E402
|
|||
# Les VLAN tenant commencent a 1000+index*10+zone (>= 1011). L'underlay reste en dessous.
|
||||
SEUIL_VLAN_TENANT = 1000
|
||||
|
||||
# Premier troisieme-octet alloue aux ZONES d'un tenant. DERIVE de la regle
|
||||
# (`base3_de` = 10.(10+index).(15+categorie)) et jamais ecrit en dur : deplacer
|
||||
# OCTET_ZONE deplacerait la frontiere de la bande basse avec lui.
|
||||
from inventory_rules import OCTET_ZONE # noqa: E402
|
||||
PREMIER_OCTET_ZONE = OCTET_ZONE + 1
|
||||
|
||||
|
||||
def chemin() -> Path | None:
|
||||
"""Emplacement du underlay.yml : SETOPS_UNDERLAY, sinon le symlink a la racine.
|
||||
|
|
@ -265,6 +291,12 @@ def octet_passerelle(plan_nomenclature: Path | None = None) -> int | None:
|
|||
return int(v) if isinstance(v, int) else None
|
||||
|
||||
|
||||
def index_site(underlay: dict | None) -> int | None:
|
||||
"""`index` du site — le seed dont derive tout son adressage (D-77). None si absent."""
|
||||
v = (underlay or {}).get("underlay", underlay or {}).get("index")
|
||||
return int(v) if isinstance(v, int) else None
|
||||
|
||||
|
||||
def _tenants_reperes() -> tuple[set[int], list[str]]:
|
||||
"""VLAN et supernets des tenants federes (import paresseux : evite le cycle avec devis)."""
|
||||
import devis_reseau
|
||||
|
|
@ -285,6 +317,9 @@ def valider(underlay: dict | None,
|
|||
if tenants_vlans is None or tenants_supernets is None:
|
||||
tenants_vlans, tenants_supernets = _tenants_reperes()
|
||||
t_supernets = [ipaddress.ip_network(s, strict=False) for s in tenants_supernets]
|
||||
# Sans `index` declare, on ignore quel supernet est le SIEN : on retombe alors sur la
|
||||
# regle stricte d'avant D-77 (aucun chevauchement), qui reste sure.
|
||||
idx_site = index_site(underlay)
|
||||
|
||||
erreurs: list[str] = []
|
||||
vus_vlan: dict[int, str] = {}
|
||||
|
|
@ -311,9 +346,29 @@ def valider(underlay: dict | None,
|
|||
except ValueError:
|
||||
erreurs.append(f"reseau '{nom}': sous_reseau '{sous}' invalide")
|
||||
continue
|
||||
# D-77 : l'underlay d'un site vit dans la BANDE BASSE de SON propre supernet.
|
||||
# Chevaucher celui d'un AUTRE tenant reste une erreur — c'est ce qui empeche deux
|
||||
# sites de se joindre. Et deborder sur la bande haute de son propre supernet en
|
||||
# est une aussi : on entrerait en collision avec ses propres zones.
|
||||
mien = ipaddress.ip_network(supernet_de(idx_site), strict=False) \
|
||||
if idx_site is not None else None
|
||||
for ts in t_supernets:
|
||||
if net.overlaps(ts):
|
||||
erreurs.append(f"reseau '{nom}': {sous} chevauche le supernet tenant {ts}")
|
||||
if not net.overlaps(ts):
|
||||
continue
|
||||
if mien is not None and ts == mien:
|
||||
# Le sien : seule la bande basse est permise.
|
||||
hauts = [o for o in range(net.network_address.packed[2],
|
||||
net.broadcast_address.packed[2] + 1)
|
||||
if o >= PREMIER_OCTET_ZONE]
|
||||
if hauts:
|
||||
erreurs.append(
|
||||
f"reseau '{nom}': {sous} deborde sur la bande des ZONES de son "
|
||||
f"propre tenant (3e octet {hauts[0]} >= {PREMIER_OCTET_ZONE}). "
|
||||
f"L'underlay tient dans 10.x.0-{PREMIER_OCTET_ZONE - 1}.x (D-77).")
|
||||
else:
|
||||
erreurs.append(f"reseau '{nom}': {sous} chevauche le supernet tenant {ts} "
|
||||
f"— c'est celui d'un AUTRE site, les deux ne pourront "
|
||||
f"jamais etre relies")
|
||||
for autre, autre_nom in vus_sous:
|
||||
if net.overlaps(autre):
|
||||
erreurs.append(f"reseau '{nom}': {sous} chevauche le reseau underlay '{autre_nom}'")
|
||||
|
|
|
|||
Loading…
Reference in a new issue