contexte : etape 2, la frontiere premier temps, les identites (P87)
La face reseau porte ses zones ; verifier_frontiere confronte supernet, administration, tunnel, alias de groupe, routes et traduction sortante a la face reseau et a la fiche du site. Aucun ecart ; cinq alterations vues. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
3d01208a68
commit
78c2c25761
8 changed files with 165 additions and 6 deletions
|
|
@ -188,7 +188,7 @@ Si `ansible-lint` n’est pas disponible, le signaler clairement. Ne pas invente
|
||||||
## Écrire, puis relire (D-68)
|
## Écrire, puis relire (D-68)
|
||||||
|
|
||||||
`--syntax-check` et `ansible-lint` prouvent que le dépôt est cohérent **avec lui-même**.
|
`--syntax-check` et `ansible-lint` prouvent que le dépôt est cohérent **avec lui-même**.
|
||||||
C'est aussi ce que font les 86 preuves de `make prouver` : elles lisent le dépôt, sans le
|
C'est aussi ce que font les 87 preuves de `make prouver` : elles lisent le dépôt, sans le
|
||||||
moindre appel réseau. **Aucune ne demande au système déployé s'il ressemble à ce que le
|
moindre appel réseau. **Aucune ne demande au système déployé s'il ressemble à ce que le
|
||||||
dépôt annonce.**
|
dépôt annonce.**
|
||||||
|
|
||||||
|
|
|
||||||
28
CHANGELOG.md
28
CHANGELOG.md
|
|
@ -1,5 +1,33 @@
|
||||||
# CHANGELOG — Set-OPS
|
# CHANGELOG — Set-OPS
|
||||||
|
|
||||||
|
## 2026-10-04 (83) — Étape 2 : la frontière, premier temps (les identités)
|
||||||
|
|
||||||
|
**Le chemin** (`docs/conception-contextes.md` §6), étape 2, dernier consommateur : la
|
||||||
|
frontière. Découpée en trois temps avec l'exploitant : les **identités** (ici), les **entrées
|
||||||
|
publiques** (redirections, règles depuis Internet), les **sorties et l'administration**.
|
||||||
|
|
||||||
|
**Fait** (`scripts/contexte.py`) :
|
||||||
|
- la face réseau porte ses **zones** : un sous-réseau par catégorie de sa nomenclature, dérivé
|
||||||
|
de son index par `sous_reseau_de`, comme la frontière et le SDN le dérivent ;
|
||||||
|
- `verifier_frontiere(site, locataire)` confronte les identités par lesquelles le devis de la
|
||||||
|
frontière désigne le locataire à ce que sa face réseau et la fiche du site portent : le
|
||||||
|
supernet (ses zones), l'administration (que la frontière range par interface, gestion et
|
||||||
|
VPN), le tunnel, **chaque alias de groupe** (8 chez chaque locataire : les groupes qui ont
|
||||||
|
des flux de bordure), une route par zone, et la traduction sortante par l'adresse publique
|
||||||
|
que le site attribue ;
|
||||||
|
- les notes que le devis imprime (« flux `externe` sans `port_public` », « sortie vers un
|
||||||
|
groupe absent de ce site ») sont retenues pendant la vérification : utiles à qui lit le
|
||||||
|
devis, du bruit dans une preuve. Elles serviront au temps suivant.
|
||||||
|
|
||||||
|
**Éprouvé** :
|
||||||
|
- **P87**, « La frontière désigne chaque locataire par les identités qu'il publie » : aucun
|
||||||
|
écart chez les deux locataires.
|
||||||
|
- `test_contexte.py`, 73 contrôles : une identité altérée de **cinq façons** (zones,
|
||||||
|
administration déclarée, tunnel, membres d'un groupe, adresse publique attribuée) est vue à
|
||||||
|
la frontière. La vérification compare bien les 8 alias de groupe de Technolibre, contrôlé à
|
||||||
|
part avant d'écrire la preuve.
|
||||||
|
- `make verifier` conforme, **87/87**. Les trois documents comptent 87 preuves.
|
||||||
|
|
||||||
## 2026-10-04 (82) — Étape 2 : les flux de chaque machine, et une information que le locataire jetait
|
## 2026-10-04 (82) — Étape 2 : les flux de chaque machine, et une information que le locataire jetait
|
||||||
|
|
||||||
**Le chemin** (`docs/conception-contextes.md` §6), étape 2, sens locataire → site : les flux.
|
**Le chemin** (`docs/conception-contextes.md` §6), étape 2, sens locataire → site : les flux.
|
||||||
|
|
|
||||||
|
|
@ -178,7 +178,8 @@ croisée, plus aucun recalcul.
|
||||||
(P85) et les flux de chaque machine (P86) existent, et disent exactement ce que les lectures
|
(P85) et les flux de chaque machine (P86) existent, et disent exactement ce que les lectures
|
||||||
croisées produisent. La première mesure des flux a trouvé une information que le locataire
|
croisées produisent. La première mesure des flux a trouvé une information que le locataire
|
||||||
jetait : les clients nommés d'un port aussi public, que Proxmox doit admettre nommément. Il
|
jetait : les clients nommés d'un port aussi public, que Proxmox doit admettre nommément. Il
|
||||||
les publie désormais (`sources_declarees`). Reste la frontière.
|
les publie désormais (`sources_declarees`). La frontière, en trois temps : les identités
|
||||||
|
(P87) sont faites ; restent les entrées publiques, puis les sorties et l'administration.
|
||||||
|
|
||||||
Méthodes du contrat : `site.fiche_pour(locataire)`, `locataire.face_reseau()`. Une classe
|
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.
|
n'ouvre jamais les fichiers de l'autre ; une preuve vérifiera la règle.
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ make placement-plan # chaque VM est-elle là où le plan la met
|
||||||
|
|
||||||
## Le trou qu'il comble
|
## Le trou qu'il comble
|
||||||
|
|
||||||
`scripts/prouver.py` porte 86 preuves (dont une conditionnelle, sautée sans la clé de la voûte). Elles sont toutes **statiques** : elles lisent le
|
`scripts/prouver.py` porte 87 preuves (dont une conditionnelle, sautée sans la clé de la voûte). Elles sont toutes **statiques** : elles lisent le
|
||||||
dépôt. Zéro appel réseau, zéro SSH, zéro `ansible`. Elles établissent que le dépôt est
|
dépôt. Zéro appel réseau, zéro SSH, zéro `ansible`. Elles établissent que le dépôt est
|
||||||
cohérent **avec lui-même** — que les handlers existent, que les intrants ont un
|
cohérent **avec lui-même** — que les handlers existent, que les intrants ont un
|
||||||
propriétaire, que rien n'est codé en dur.
|
propriétaire, que rien n'est codé en dur.
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,7 @@ Nommer un point ouvert vaut mieux qu'une ligne rassurante sans mécanisme derri
|
||||||
Aucune de ces responsabilités n'est laissée à la parole :
|
Aucune de ces responsabilités n'est laissée à la parole :
|
||||||
|
|
||||||
```
|
```
|
||||||
make prouver le dépôt est-il cohérent avec lui-même (86 preuves, zéro réseau)
|
make prouver le dépôt est-il cohérent avec lui-même (87 preuves, zéro réseau)
|
||||||
make remise-verifier le second temps de la remise est-il fait, ou en retard ? (P80)
|
make remise-verifier le second temps de la remise est-il fait, ou en retard ? (P80)
|
||||||
make certificats-plan ce que le disque porte contre ce que la mémoire sert
|
make certificats-plan ce que le disque porte contre ce que la mémoire sert
|
||||||
make expositions-plan chaque service publié répond-il, et depuis où
|
make expositions-plan chaque service publié répond-il, et depuis où
|
||||||
|
|
|
||||||
|
|
@ -434,11 +434,18 @@ class Locataire(Ecosysteme):
|
||||||
self.dossier_inventaire() / "group_vars" / "serveur_backup.yml")}
|
self.dossier_inventaire() / "group_vars" / "serveur_backup.yml")}
|
||||||
sources.update({f"flux-genere/{h}.connectivite.json":
|
sources.update({f"flux-genere/{h}.connectivite.json":
|
||||||
_empreinte(self.depot / "flux-genere" / f"{h}.connectivite.json") for h in flux})
|
_empreinte(self.depot / "flux-genere" / f"{h}.connectivite.json") for h in flux})
|
||||||
|
# Ses zones : un sous-reseau par categorie de sa nomenclature, derive de son index
|
||||||
|
# (`sous_reseau_de`, comme la frontiere et le SDN le derivent).
|
||||||
|
from inventory_rules import sous_reseau_de
|
||||||
|
cats = self.nomenclature().get("categories") or {}
|
||||||
|
zones = ([sous_reseau_de(self.index, int(z)) for z in sorted(cats, key=int)]
|
||||||
|
if self.index is not None else [])
|
||||||
return {
|
return {
|
||||||
"locataire": self.nom,
|
"locataire": self.nom,
|
||||||
"hebergeur": self.nom_hebergeur(),
|
"hebergeur": self.nom_hebergeur(),
|
||||||
"sources": sources,
|
"sources": sources,
|
||||||
"index": self.index,
|
"index": self.index,
|
||||||
|
"zones": zones,
|
||||||
"machines": machines,
|
"machines": machines,
|
||||||
"groupes": groupes,
|
"groupes": groupes,
|
||||||
"administration": {"intrant": intrant, "tunnel": tunnel_admin_de(self.depot),
|
"administration": {"intrant": intrant, "tunnel": tunnel_admin_de(self.depot),
|
||||||
|
|
@ -714,6 +721,72 @@ def verifier_flux(site: "Site", locataire: "Locataire", face: dict | None = None
|
||||||
return ecarts
|
return ecarts
|
||||||
|
|
||||||
|
|
||||||
|
def verifier_frontiere(site: "Site", locataire: "Locataire", face: dict | None = None,
|
||||||
|
fiche: dict | None = None) -> list[str]:
|
||||||
|
"""La frontiere, premier temps : les IDENTITES par lesquelles elle designe ce locataire.
|
||||||
|
|
||||||
|
Ses alias (supernet, administration par interface, tunnel, un alias par groupe qui a des
|
||||||
|
flux de bordure), ses routes, et sa traduction sortante. Chacun doit dire ce que la face
|
||||||
|
reseau du locataire (zones, groupes, administration) et la fiche du site (adresse publique
|
||||||
|
attribuee) portent deja. Les entrees publiques et les sorties sont les temps suivants.
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
import devis_opnsense
|
||||||
|
import devis_reseau
|
||||||
|
f = face if face is not None else locataire.face_reseau()
|
||||||
|
fi = fiche if fiche is not None else site.fiche_pour(locataire)
|
||||||
|
with _monter(site):
|
||||||
|
tenants = devis_reseau.decouvrir_du_site()
|
||||||
|
etq = next((p + str(n["index"]) for nom, p, n in tenants if nom == locataire.nom), None)
|
||||||
|
# Le devis imprime ses notes (flux sans `port_public`, sorties vers un absent) : utiles a
|
||||||
|
# qui le lit, du bruit dans une preuve. On les retient ici.
|
||||||
|
import io
|
||||||
|
with contextlib.redirect_stderr(io.StringIO()):
|
||||||
|
devis = devis_opnsense.construire(tenants) if etq else {}
|
||||||
|
if not etq:
|
||||||
|
return ["la frontiere ne decouvre pas ce locataire"]
|
||||||
|
ecarts: list[str] = []
|
||||||
|
alias = devis.get("alias") or {}
|
||||||
|
ips = {h: m["ip"] for h, m in f["machines"].items()}
|
||||||
|
|
||||||
|
# Le supernet : ses zones.
|
||||||
|
sup = (alias.get(f"SETOPS_TENANT_{etq}") or {}).get("contenu")
|
||||||
|
if sorted(sup or []) != sorted(f["zones"]):
|
||||||
|
ecarts.append(f"SETOPS_TENANT_{etq} : la frontiere porte {sup}, la face {f['zones']}")
|
||||||
|
# L'administration : l'intrant, que la frontiere range par interface, et le tunnel.
|
||||||
|
adm = sorted({c for k, v in alias.items() if re.fullmatch(rf"SETOPS_ADMIN_{etq}_(?!TUNNEL)\w+", k)
|
||||||
|
for c in v.get("contenu") or []})
|
||||||
|
if adm != sorted(f["administration"]["intrant"]):
|
||||||
|
ecarts.append(f"administration : la frontiere range {adm}, la face {sorted(f['administration']['intrant'])}")
|
||||||
|
tun = sorted((alias.get(f"SETOPS_ADMIN_{etq}_TUNNEL") or {}).get("contenu") or [])
|
||||||
|
if tun != sorted(f["administration"]["tunnel"]):
|
||||||
|
ecarts.append(f"tunnel : la frontiere porte {tun}, la face {sorted(f['administration']['tunnel'])}")
|
||||||
|
# Un alias par groupe : ses membres sont ceux du groupe dans la face.
|
||||||
|
for k, v in sorted(alias.items()):
|
||||||
|
m = re.search(r"portant (\S+) \(instance (\S+)\)", str(v.get("description", "")))
|
||||||
|
if not m or m.group(2) != locataire.nom:
|
||||||
|
continue
|
||||||
|
role = m.group(1)
|
||||||
|
attendu = sorted({ips[h] for h in f["groupes"].get(role, [])})
|
||||||
|
if role not in f["groupes"]:
|
||||||
|
ecarts.append(f"{k} : groupe {role} inconnu de la face")
|
||||||
|
elif sorted(v.get("contenu") or []) != attendu:
|
||||||
|
ecarts.append(f"{k} : la frontiere porte {sorted(v.get('contenu') or [])}, la face {attendu}")
|
||||||
|
# Les routes : une par zone.
|
||||||
|
routes = sorted(r["reseau"] for r in devis.get("routes") or [] if r.get("tenant") == locataire.nom)
|
||||||
|
if routes != sorted(f["zones"]):
|
||||||
|
ecarts.append(f"routes : la frontiere route {routes}, la face a pour zones {sorted(f['zones'])}")
|
||||||
|
# La traduction sortante : sa flotte sort par l'adresse que le site lui attribue.
|
||||||
|
nat = [x for x in devis.get("nat") or [] if x.get("tenant") == locataire.nom]
|
||||||
|
cibles = sorted(str(x.get("cible")) for x in nat)
|
||||||
|
if cibles != [str(fi["attribue"]["ip_publique"])]:
|
||||||
|
ecarts.append(f"traduction sortante : la frontiere fait sortir par {cibles}, "
|
||||||
|
f"la fiche attribue {fi['attribue']['ip_publique']}")
|
||||||
|
if any(x.get("source") != f"SETOPS_TENANT_{etq}" for x in nat):
|
||||||
|
ecarts.append("traduction sortante : la source n'est pas le supernet du locataire")
|
||||||
|
return ecarts
|
||||||
|
|
||||||
|
|
||||||
def couples_hebergeur() -> list[tuple["Site", "Locataire"]]:
|
def couples_hebergeur() -> list[tuple["Site", "Locataire"]]:
|
||||||
"""Chaque (site, locataire) dont ce site est l'hebergeur ACTIF — les seuls qui ont une fiche
|
"""Chaque (site, locataire) dont ce site est l'hebergeur ACTIF — les seuls qui ont une fiche
|
||||||
a verifier : un site de reprise ne depose rien tant que la bascule n'a pas eu lieu."""
|
a verifier : un site de reprise ne depose rien tant que la bascule n'a pas eu lieu."""
|
||||||
|
|
@ -812,7 +885,8 @@ def main(argv: list[str] | None = None) -> int:
|
||||||
for site, loc in couples:
|
for site, loc in couples:
|
||||||
face = loc.face_reseau()
|
face = loc.face_reseau()
|
||||||
for quoi, e in (("faits", verifier_face(site, loc, face)),
|
for quoi, e in (("faits", verifier_face(site, loc, face)),
|
||||||
("flux ", verifier_flux(site, loc, face))):
|
("flux ", verifier_flux(site, loc, face)),
|
||||||
|
("frontiere (identites)", verifier_frontiere(site, loc, face))):
|
||||||
total += len(e)
|
total += len(e)
|
||||||
print(f"{'OK ' if not e else 'ECART'} {loc.nom} -> {site.nom} ({quoi})"
|
print(f"{'OK ' if not e else 'ECART'} {loc.nom} -> {site.nom} ({quoi})"
|
||||||
+ "".join(f"\n - {x}" for x in e))
|
+ "".join(f"\n - {x}" for x in e))
|
||||||
|
|
|
||||||
|
|
@ -4740,6 +4740,29 @@ def preuve_flux_concordent() -> tuple[bool, str]:
|
||||||
f"Proxmox admettent les memes sources.")
|
f"Proxmox admettent les memes sources.")
|
||||||
|
|
||||||
|
|
||||||
|
def preuve_frontiere_identites() -> tuple[bool, str]:
|
||||||
|
"""Les alias, les routes et la traduction sortante par lesquels la frontiere designe chaque
|
||||||
|
locataire disent-ils ce que sa face reseau et la fiche du site portent ?
|
||||||
|
|
||||||
|
POURQUOI (2026-10-04, `docs/conception-contextes.md` §2.4). La frontiere est le dernier
|
||||||
|
consommateur du site qui lit l'interieur des locataires. Premier temps de sa bascule : les
|
||||||
|
identites — supernet (ses zones), administration par interface, tunnel, un alias par groupe
|
||||||
|
qui a des flux de bordure, une route par zone, la sortie par l'adresse publique attribuee.
|
||||||
|
Les entrees publiques et les sorties suivront.
|
||||||
|
"""
|
||||||
|
sys.path.insert(0, str(RACINE / "scripts"))
|
||||||
|
import contexte as _ctx
|
||||||
|
couples = _ctx.couples_hebergeur()
|
||||||
|
if not couples:
|
||||||
|
return True, "Aucun site frere : aucune frontiere a confronter."
|
||||||
|
ecarts = [f"{l.nom} : {e}" for s, l in couples for e in _ctx.verifier_frontiere(s, l)]
|
||||||
|
if ecarts:
|
||||||
|
return False, (f"{len(ecarts)} ecart(s) entre la frontiere et ce que le locataire publie : "
|
||||||
|
+ " | ".join(ecarts[:4]))
|
||||||
|
return True, (f"{len(couples)} locataire(s) : la frontiere les designe par les identites qu'ils "
|
||||||
|
f"publient (supernet, administration, tunnel, groupes, routes, sortie).")
|
||||||
|
|
||||||
|
|
||||||
PREUVES: list[dict] = [
|
PREUVES: list[dict] = [
|
||||||
{"id": "P01", "titre": "Lint (ansible-lint)", "refs": ["AFF-006"],
|
{"id": "P01", "titre": "Lint (ansible-lint)", "refs": ["AFF-006"],
|
||||||
"cmds": [["ansible-lint", "-q"]]},
|
"cmds": [["ansible-lint", "-q"]]},
|
||||||
|
|
@ -4932,6 +4955,8 @@ PREUVES: list[dict] = [
|
||||||
"refs": [], "func": preuve_face_reseau},
|
"refs": [], "func": preuve_face_reseau},
|
||||||
{"id": "P86", "titre": "Machine par machine, le locataire et Proxmox admettent les memes entrees",
|
{"id": "P86", "titre": "Machine par machine, le locataire et Proxmox admettent les memes entrees",
|
||||||
"refs": [], "func": preuve_flux_concordent},
|
"refs": [], "func": preuve_flux_concordent},
|
||||||
|
{"id": "P87", "titre": "La frontiere designe chaque locataire par les identites qu'il publie",
|
||||||
|
"refs": [], "func": preuve_frontiere_identites},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,9 @@ site lui attribue, et nomme un hebergeur qui existe ; la fiche de chaque heberge
|
||||||
exactement ce que son locataire porte, et une fiche alteree (sept facons) est vue en ecart ; la face
|
exactement ce que son locataire porte, et une fiche alteree (sept facons) est vue en ecart ; la face
|
||||||
reseau de chaque locataire dit ce que les consommateurs du site en tirent, et une face alteree
|
reseau de chaque locataire dit ce que les consommateurs du site en tirent, et une face alteree
|
||||||
(huit facons) est vue en ecart ; machine par machine, le pare-feu du locataire et celui de
|
(huit facons) est vue en ecart ; machine par machine, le pare-feu du locataire et celui de
|
||||||
Proxmox admettent les memes entrees, et des flux alteres (quatre facons) sont vus en ecart.
|
Proxmox admettent les memes entrees, et des flux alteres (quatre facons) sont vus en ecart ;
|
||||||
|
la frontiere designe chaque locataire par les memes identites, et une identite alteree (cinq
|
||||||
|
facons) se voit.
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
@ -282,6 +284,34 @@ def flux() -> None:
|
||||||
verifier(bool(C.verifier_flux(site, loc, f)), f"des flux alteres ({nom}) sont vus en ecart")
|
verifier(bool(C.verifier_flux(site, loc, f)), f"des flux alteres ({nom}) sont vus en ecart")
|
||||||
|
|
||||||
|
|
||||||
|
def frontiere() -> None:
|
||||||
|
"""La frontiere designe chaque locataire par les memes identites que sa face reseau et la
|
||||||
|
fiche du site — et chaque identite alteree se voit."""
|
||||||
|
import copy
|
||||||
|
couples = C.couples_hebergeur()
|
||||||
|
if not couples:
|
||||||
|
print("SAUTE aucun site frere : aucune frontiere a confronter")
|
||||||
|
return
|
||||||
|
for site, loc in couples:
|
||||||
|
e = C.verifier_frontiere(site, loc)
|
||||||
|
verifier(not e, f"frontiere {loc.nom} : alias, routes et traduction sortante concordent"
|
||||||
|
+ (f" ({e[0]})" if e else ""))
|
||||||
|
site, loc = couples[0]
|
||||||
|
face, fiche = loc.face_reseau(), site.fiche_pour(loc)
|
||||||
|
groupe = next(g for g in ("serveur_postfix", "serveur_dovecot", "serveur_nginx") if face["groupes"].get(g))
|
||||||
|
alterations = {
|
||||||
|
"zones": lambda f, fi: f.__setitem__("zones", f["zones"][:-1]),
|
||||||
|
"administration declaree": lambda f, fi: f["administration"].__setitem__("intrant", ["192.0.2.0/24"]),
|
||||||
|
"tunnel": lambda f, fi: f["administration"].__setitem__("tunnel", ["192.0.2.128/25"]),
|
||||||
|
f"membres du groupe {groupe}": lambda f, fi: f["groupes"].__setitem__(groupe, []),
|
||||||
|
"adresse publique attribuee": lambda f, fi: fi["attribue"].__setitem__("ip_publique", "192.0.2.60"),
|
||||||
|
}
|
||||||
|
for nom, alterer in alterations.items():
|
||||||
|
f, fi = copy.deepcopy(face), copy.deepcopy(fiche)
|
||||||
|
alterer(f, fi)
|
||||||
|
verifier(bool(C.verifier_frontiere(site, loc, f, fi)), f"une identite alteree ({nom}) est vue a la frontiere")
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
d = Path(tmp)
|
d = Path(tmp)
|
||||||
|
|
@ -293,6 +323,7 @@ def main() -> int:
|
||||||
fiches()
|
fiches()
|
||||||
faces()
|
faces()
|
||||||
flux()
|
flux()
|
||||||
|
frontiere()
|
||||||
if ECHECS:
|
if ECHECS:
|
||||||
print(f"\n{len(ECHECS)} echec(s).")
|
print(f"\n{len(ECHECS)} echec(s).")
|
||||||
return 1
|
return 1
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue