contexte : etape 2a, la fiche du site et la preuve qu elle dit vrai
site.fiche_pour(locataire) reunit attribution, offre, racine et les trois lectures directes de l instancier ; verifier_fiche la confronte aux copies, a la racine et a l inventaire du locataire. P84 : aucune ecart sur les deux couples ; sept alterations vues en ecart. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
c891d7fb26
commit
8251022148
7 changed files with 333 additions and 8 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 83 preuves de `make prouver` : elles lisent le dépôt, sans le
|
C'est aussi ce que font les 84 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.**
|
||||||
|
|
||||||
|
|
|
||||||
39
CHANGELOG.md
39
CHANGELOG.md
|
|
@ -1,5 +1,44 @@
|
||||||
# CHANGELOG — Set-OPS
|
# CHANGELOG — Set-OPS
|
||||||
|
|
||||||
|
## 2026-10-04 (80) — Étape 2, première moitié : la fiche du site, et la preuve qu'elle dit vrai
|
||||||
|
|
||||||
|
**Le chemin** (`docs/conception-contextes.md` §6), étape 2 : générer les fiches **à côté** de
|
||||||
|
l'existant, et prouver qu'elles disent exactement ce que les lectures croisées produisent.
|
||||||
|
Première moitié : le sens site → locataire. La face réseau du locataire suit.
|
||||||
|
|
||||||
|
**Fait** (`scripts/contexte.py`) :
|
||||||
|
- `site.fiche_pour(locataire)` réunit ce que trois canaux portaient sans fiche : ce que le site
|
||||||
|
**attribue** (index, adresse publique), ce qu'il **offre** (les intrants de service,
|
||||||
|
calculés par `site_intrants.contrat()`), sa **racine de confiance** (le certificat
|
||||||
|
lui-même), et ce que l'instancier **lisait directement** dans le site (zone et résolveurs à
|
||||||
|
déléguer, serveurs de temps, mode de routage), par les mêmes fonctions qu'aujourd'hui. La
|
||||||
|
fiche porte l'empreinte de ses quatre sources.
|
||||||
|
- `verifier_fiche(site, locataire)` confronte la fiche à ce que le locataire porte : ses
|
||||||
|
copies (la table de `site_intrants`), sa racine, et son inventaire généré (`chrony_serveurs`
|
||||||
|
sur chaque machine, la délégation DNS sur la machine qui résout, le rattachement SDN).
|
||||||
|
- `locataire.domaine_interne()`, lu comme le lit `instancier._domaine_interne`. La première
|
||||||
|
version le cherchait dans `00-instance.yml` ; il est dans `10-intrants.yml`. La comparaison
|
||||||
|
passait quand même, parce que les domaines du site et du locataire diffèrent : relevé et
|
||||||
|
corrigé avant le commit.
|
||||||
|
- `_monter(site, locataire)` : les modules d'avant lisent « le site monté » ; la fiche leur
|
||||||
|
désigne le site voulu le temps de l'appel, puis remet l'environnement, même sur exception.
|
||||||
|
Passerelle de transition, jusqu'à l'étape 3.
|
||||||
|
- `python3 scripts/contexte.py --fiche <locataire>` affiche la fiche ;
|
||||||
|
`--verifier-fiches` confronte chaque couple dont le site est l'hébergeur **actif** (un site
|
||||||
|
de reprise ne dépose rien tant que la bascule n'a pas eu lieu).
|
||||||
|
|
||||||
|
**Rien n'est encore déposé ni consommé** : la fiche est calculée, pas écrite chez le locataire.
|
||||||
|
Le dépôt et la bascule de l'instancier sont l'étape 3, à inventaire identique.
|
||||||
|
|
||||||
|
**Éprouvé** :
|
||||||
|
- **P84**, « La fiche du site dit exactement ce que chaque locataire porte » : 2 fiches,
|
||||||
|
SITE-Chezlepro → OPS-Chezlepro et → OPS-Technolibre, aucun écart.
|
||||||
|
- `test_contexte.py`, 50 contrôles : les deux fiches réelles concordent, et une fiche altérée
|
||||||
|
de **sept façons** (index, adresse publique, intrant de service, racine, serveurs de temps,
|
||||||
|
délégation DNS, mode de routage) est vue en écart à chaque fois.
|
||||||
|
- `make verifier` conforme, **84/84**. `AGENTS.md`, `devis-services.md` et
|
||||||
|
`responsabilites-locataire-hebergeur.md` comptent désormais 84 preuves.
|
||||||
|
|
||||||
## 2026-10-04 (79) — Étape 1 : le module `contexte.py`, que rien n'utilise encore
|
## 2026-10-04 (79) — Étape 1 : le module `contexte.py`, que rien n'utilise encore
|
||||||
|
|
||||||
**Le chemin** (`docs/conception-contextes.md` §6), premier pas : le tronc commun et les deux
|
**Le chemin** (`docs/conception-contextes.md` §6), premier pas : le tronc commun et les deux
|
||||||
|
|
|
||||||
|
|
@ -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 83 preuves (dont une conditionnelle, sautée sans la clé de la voûte). Elles sont toutes **statiques** : elles lisent le
|
`scripts/prouver.py` porte 84 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 (83 preuves, zéro réseau)
|
make prouver le dépôt est-il cohérent avec lui-même (84 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ù
|
||||||
|
|
|
||||||
|
|
@ -12,18 +12,24 @@ site affichant zero machine, `make ci` melangeant le modele public et les ecosys
|
||||||
Ce module est le SEUL endroit ou se lisent ces indices. Les scripts lui demandent « ou
|
Ce module est le SEUL endroit ou se lisent ces indices. Les scripts lui demandent « ou
|
||||||
suis-je ? » et recoivent un objet : un `Site` ou un `Locataire`, qui heritent d'`Ecosysteme`.
|
suis-je ? » et recoivent un objet : un `Site` ou un `Locataire`, qui heritent d'`Ecosysteme`.
|
||||||
|
|
||||||
ETAPE 1 DU CHEMIN (§6) : le module existe, il est teste, et RIEN NE L'UTILISE ENCORE. Les
|
ETAPES 1 ET 2 DU CHEMIN (§6) : le module existe, il est teste, et RIEN NE L'UTILISE ENCORE.
|
||||||
scripts l'adopteront un par un, chacun a resultat identique. Les fiches du contrat
|
La fiche du site (`site.fiche_pour(locataire)`) est calculee A COTE de l'existant, et
|
||||||
(`site.fiche_pour`, `locataire.face_reseau`) sont l'etape 2.
|
`verifier_fiche` prouve qu'elle dit exactement ce que le locataire porte aujourd'hui. La face
|
||||||
|
reseau du locataire suit. Les consommateurs basculeront a l'etape 3, a resultat identique.
|
||||||
|
|
||||||
Usage :
|
Usage :
|
||||||
python3 scripts/contexte.py # le contexte actif, et ce que chaque indice dit
|
python3 scripts/contexte.py # le contexte actif, et ce que chaque indice dit
|
||||||
python3 scripts/contexte.py --json
|
python3 scripts/contexte.py --json
|
||||||
|
python3 scripts/contexte.py --fiche OPS-Technolibre # la fiche que son hebergeur lui destine
|
||||||
|
python3 scripts/contexte.py --verifier-fiches # chaque fiche dit-elle ce que le locataire porte ?
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import contextlib
|
||||||
|
import hashlib
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
@ -53,6 +59,35 @@ def _yaml(chemin: Path) -> dict:
|
||||||
return yaml.safe_load(chemin.read_text(encoding="utf-8")) or {}
|
return yaml.safe_load(chemin.read_text(encoding="utf-8")) or {}
|
||||||
|
|
||||||
|
|
||||||
|
def _empreinte(chemin: Path) -> str:
|
||||||
|
return hashlib.sha256(chemin.read_bytes()).hexdigest() if chemin.is_file() else ""
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def _monter(site: "Site | None" = None, locataire: "Locataire | None" = None):
|
||||||
|
"""Monte CE site et CE locataire le temps d'interroger les modules d'avant.
|
||||||
|
|
||||||
|
`underlay`, `site_intrants`, `instancier` lisent « le site monte » et « l'instance
|
||||||
|
courante » (variables `SETOPS_*`, sinon les liens). Plutot que de recopier leurs calculs,
|
||||||
|
on leur designe l'ecosysteme voulu, puis on remet l'environnement tel quel. C'est une
|
||||||
|
passerelle de transition : a l'etape 3, ces modules recevront leurs objets.
|
||||||
|
"""
|
||||||
|
clefs = ("SETOPS_UNDERLAY", "SETOPS_INSTANCE")
|
||||||
|
avant = {k: os.environ.get(k) for k in clefs}
|
||||||
|
try:
|
||||||
|
if site is not None:
|
||||||
|
os.environ["SETOPS_UNDERLAY"] = str(site.underlay)
|
||||||
|
if locataire is not None:
|
||||||
|
os.environ["SETOPS_INSTANCE"] = str(locataire.depot)
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
for k, v in avant.items():
|
||||||
|
if v is None:
|
||||||
|
os.environ.pop(k, None)
|
||||||
|
else:
|
||||||
|
os.environ[k] = v
|
||||||
|
|
||||||
|
|
||||||
# --- LE TRONC COMMUN ----------------------------------------------------------------------
|
# --- LE TRONC COMMUN ----------------------------------------------------------------------
|
||||||
|
|
||||||
class Ecosysteme:
|
class Ecosysteme:
|
||||||
|
|
@ -210,6 +245,50 @@ class Site(Ecosysteme):
|
||||||
presents = {l.nom for l in self.locataires()}
|
presents = {l.nom for l in self.locataires()}
|
||||||
return [n for n in self.noms_locataires() if n not in presents]
|
return [n for n in self.noms_locataires() if n not in presents]
|
||||||
|
|
||||||
|
# LE CONTRAT, SENS SITE -> LOCATAIRE (§2.4) ---------------------------------------------
|
||||||
|
def fiche_pour(self, locataire: "Locataire") -> dict:
|
||||||
|
"""Ce que CE site attribue et offre a CE locataire, en une fiche.
|
||||||
|
|
||||||
|
Trois canaux le portaient jusqu'ici, sans fiche (relevé du 2026-10-04) :
|
||||||
|
- des COPIES a la main : index, adresse publique, intrants de service, racine ;
|
||||||
|
- une LECTURE DIRECTE du site par `instancier.py` au moment de generer l'inventaire :
|
||||||
|
delegation DNS, serveurs de temps, mode de routage ;
|
||||||
|
- le genome (hors fiche : il passe par la forge).
|
||||||
|
La fiche les reunit, calcules par LES MEMES fonctions qu'aujourd'hui. Elle porte
|
||||||
|
l'empreinte de ses sources : une fiche qui ne correspond plus a son site se voit.
|
||||||
|
"""
|
||||||
|
import site_intrants as si
|
||||||
|
with _monter(self, locataire):
|
||||||
|
contrat = si.contrat()
|
||||||
|
carte = underlay_mod.charger() or {}
|
||||||
|
intrants = underlay_mod.lire_plan_site("10-intrants.yml")
|
||||||
|
# Les trois lectures d'`instancier.py`, a l'identique (`_zones_deleguees`, la patte
|
||||||
|
# de transit de la frontiere, `routage_tenants`).
|
||||||
|
zone = str(intrants.get("domaine_interne") or "").strip()
|
||||||
|
resolveurs = [a for a in underlay_mod.adresses_site_portant("serveur_resolveur") if a]
|
||||||
|
transit = underlay_mod.reseau_transit(carte) or {}
|
||||||
|
reseaux_transit = {str(transit.get("nom"))} if transit.get("nom") else set()
|
||||||
|
temps = sorted({str(h["ip"]) for h in underlay_mod.hotes(carte)
|
||||||
|
if h.get("role") == "frontiere" and h.get("ip")
|
||||||
|
and str(h.get("reseau")) in reseaux_transit
|
||||||
|
and str(h.get("etat", "actif")) != "reserve"})
|
||||||
|
routage = underlay_mod.routage_tenants(carte)
|
||||||
|
ip_publique = si.ip_publique_de(locataire.nom)
|
||||||
|
racine = self.depot / "ac-racine-site.crt"
|
||||||
|
offre = {k: v for k, v in contrat.items() if k not in ("ip_publique", "forge_amont_ac")}
|
||||||
|
return {
|
||||||
|
"site": self.nom,
|
||||||
|
"locataire": locataire.nom,
|
||||||
|
"sources": {f: _empreinte(self.depot / f) for f in
|
||||||
|
("underlay.yml", "plan/10-intrants.yml", "opnsense.yml", "ac-racine-site.crt")},
|
||||||
|
"attribue": {"index": self.allocations().get(locataire.nom), "ip_publique": ip_publique},
|
||||||
|
"offre": offre,
|
||||||
|
"racine_confiance": {"fichier": racine.name, "sha256": _empreinte(racine),
|
||||||
|
"pem": racine.read_text(encoding="utf-8") if racine.is_file() else ""},
|
||||||
|
"generation": {"zone_site": zone, "resolveurs_site": resolveurs,
|
||||||
|
"serveurs_temps": temps, "routage_tenants": routage},
|
||||||
|
}
|
||||||
|
|
||||||
def decrire(self) -> dict:
|
def decrire(self) -> dict:
|
||||||
d = super().decrire()
|
d = super().decrire()
|
||||||
d.update({"locataires": self.noms_locataires(),
|
d.update({"locataires": self.noms_locataires(),
|
||||||
|
|
@ -258,6 +337,19 @@ class Locataire(Ecosysteme):
|
||||||
def voute(self) -> Path:
|
def voute(self) -> Path:
|
||||||
return self.dossier_inventaire() / "group_vars" / "all" / "vault.yml"
|
return self.dossier_inventaire() / "group_vars" / "all" / "vault.yml"
|
||||||
|
|
||||||
|
def domaine_interne(self) -> str:
|
||||||
|
"""Son domaine interne : le premier `domaine_interne` non vide de `group_vars/all`,
|
||||||
|
dans l'ordre des fichiers — la lecture meme d'`instancier._domaine_interne`."""
|
||||||
|
base = self.dossier_inventaire() / "group_vars" / "all"
|
||||||
|
for fichier in sorted(base.glob("*.yml")) if base.is_dir() else []:
|
||||||
|
try:
|
||||||
|
d = yaml.safe_load(fichier.read_text(encoding="utf-8")) or {}
|
||||||
|
except yaml.YAMLError:
|
||||||
|
continue
|
||||||
|
if isinstance(d, dict) and str(d.get("domaine_interne", "")).strip():
|
||||||
|
return str(d["domaine_interne"]).strip()
|
||||||
|
return ""
|
||||||
|
|
||||||
def nom_hebergeur(self) -> str | None:
|
def nom_hebergeur(self) -> str | None:
|
||||||
h = ((self.filiation.get("depots") or {}).get("hebergeur") or {}).get("nom")
|
h = ((self.filiation.get("depots") or {}).get("hebergeur") or {}).get("nom")
|
||||||
return str(h) if h else None
|
return str(h) if h else None
|
||||||
|
|
@ -275,6 +367,102 @@ class Locataire(Ecosysteme):
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
# --- LA FICHE DIT-ELLE CE QUE LE LOCATAIRE PORTE ? ----------------------------------------
|
||||||
|
|
||||||
|
def _hostvars(inventaire: Path) -> dict:
|
||||||
|
d = _yaml(inventaire)
|
||||||
|
hv: dict = {}
|
||||||
|
|
||||||
|
def parcourir(g: dict) -> None:
|
||||||
|
for h, v in (g.get("hosts") or {}).items():
|
||||||
|
hv.setdefault(h, {}).update(v or {})
|
||||||
|
for c in (g.get("children") or {}).values():
|
||||||
|
parcourir(c or {})
|
||||||
|
parcourir(d.get("all") or {})
|
||||||
|
return hv
|
||||||
|
|
||||||
|
|
||||||
|
def verifier_fiche(site: "Site", locataire: "Locataire", fiche: dict | None = None) -> list[str]:
|
||||||
|
"""Les ecarts entre la fiche du site et ce que le locataire porte AUJOURD'HUI.
|
||||||
|
|
||||||
|
Vide = la fiche dit exactement ce que les trois canaux d'avant ont depose chez le locataire :
|
||||||
|
ses copies (intrants, index, adresse publique, racine) et son inventaire genere (temps,
|
||||||
|
delegation DNS, rattachement SDN). C'est la condition pour que l'instancier puisse, a
|
||||||
|
l'etape 3, lire la fiche au lieu du site sans que l'inventaire change d'un octet.
|
||||||
|
"""
|
||||||
|
import site_intrants as si
|
||||||
|
f = fiche if fiche is not None else site.fiche_pour(locataire)
|
||||||
|
ecarts: list[str] = []
|
||||||
|
|
||||||
|
# 1. L'attribution.
|
||||||
|
if f["attribue"]["index"] != locataire.index:
|
||||||
|
ecarts.append(f"index : le site attribue {f['attribue']['index']}, "
|
||||||
|
f"le locataire porte {locataire.index}")
|
||||||
|
|
||||||
|
# 2. L'offre et l'adresse publique, contre les copies du locataire (la table de `site_intrants`).
|
||||||
|
with _monter(site, locataire):
|
||||||
|
vu = si._declare_par_le_locataire()
|
||||||
|
valeurs = dict(f["offre"], ip_publique=f["attribue"]["ip_publique"])
|
||||||
|
for clef, copie in vu.items():
|
||||||
|
attendu = valeurs.get(clef)
|
||||||
|
if not attendu or "{{" in str(copie):
|
||||||
|
continue
|
||||||
|
if si._normaliser(copie) != si._normaliser(attendu):
|
||||||
|
fichier, variable = si.OU_LE_LOCATAIRE_LE_DIT[clef]
|
||||||
|
ecarts.append(f"{variable} ({fichier}) : le locataire dit « {copie} », la fiche « {attendu} »")
|
||||||
|
|
||||||
|
# 3. La racine de confiance.
|
||||||
|
copie_ac = locataire.depot / "ac-racine-site.crt"
|
||||||
|
if copie_ac.is_file() and _empreinte(copie_ac) != f["racine_confiance"]["sha256"]:
|
||||||
|
ecarts.append("ac-racine-site.crt : la copie du locataire n'est pas la racine du site")
|
||||||
|
|
||||||
|
# 4. Ce que l'instancier a lu dans le site, tel qu'il l'a ecrit dans l'inventaire.
|
||||||
|
hv = _hostvars(locataire.inventaire())
|
||||||
|
if not hv:
|
||||||
|
ecarts.append(f"inventaire introuvable ou vide : {locataire.inventaire()}")
|
||||||
|
return ecarts
|
||||||
|
gen = f["generation"]
|
||||||
|
temps = gen["serveurs_temps"] or None
|
||||||
|
for h, v in sorted(hv.items()):
|
||||||
|
if v.get("chrony_serveurs") != temps:
|
||||||
|
ecarts.append(f"{h} : chrony_serveurs = {v.get('chrony_serveurs')}, la fiche dit {temps}")
|
||||||
|
break
|
||||||
|
domaine_loc = locataire.domaine_interne()
|
||||||
|
attendu_zones = ([{"adresses": gen["resolveurs_site"], "nom": gen["zone_site"]}]
|
||||||
|
if gen["zone_site"] and gen["resolveurs_site"] and gen["zone_site"] != domaine_loc
|
||||||
|
else [])
|
||||||
|
portees = [v["serveur_resolveur_zones_deleguees"] for v in hv.values()
|
||||||
|
if "serveur_resolveur_zones_deleguees" in v]
|
||||||
|
normal = [sorted(({"adresses": list(z.get("adresses") or []), "nom": z.get("nom")} for z in p),
|
||||||
|
key=lambda z: str(z["nom"])) for p in portees]
|
||||||
|
if attendu_zones and not normal:
|
||||||
|
ecarts.append("delegation DNS : la fiche en offre une, l'inventaire n'en porte aucune")
|
||||||
|
for p in normal:
|
||||||
|
if p != attendu_zones and (attendu_zones or p):
|
||||||
|
ecarts.append(f"delegation DNS : l'inventaire porte {p}, la fiche dit {attendu_zones}")
|
||||||
|
break
|
||||||
|
sdn = gen["routage_tenants"] == "sdn"
|
||||||
|
for h, v in sorted(hv.items()):
|
||||||
|
if sdn and (not v.get("proxmox_pont") or v.get("proxmox_etiquette_vlan")):
|
||||||
|
ecarts.append(f"{h} : routage SDN selon la fiche, mais pont « {v.get('proxmox_pont')} », "
|
||||||
|
f"etiquette « {v.get('proxmox_etiquette_vlan')} »")
|
||||||
|
break
|
||||||
|
if not sdn and v.get("proxmox_pont"):
|
||||||
|
ecarts.append(f"{h} : routage « {gen['routage_tenants']} » selon la fiche, mais un pont SDN")
|
||||||
|
break
|
||||||
|
return ecarts
|
||||||
|
|
||||||
|
|
||||||
|
def couples_hebergeur() -> list[tuple["Site", "Locataire"]]:
|
||||||
|
"""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."""
|
||||||
|
out = []
|
||||||
|
for underlay in sorted(DOSSIER_ECOSYSTEMES.glob("*/underlay.yml")):
|
||||||
|
site = Site(underlay.parent)
|
||||||
|
out += [(site, loc) for loc in site.locataires() if loc.nom_hebergeur() == site.nom]
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
# --- LE CONTEXTE ACTIF --------------------------------------------------------------------
|
# --- LE CONTEXTE ACTIF --------------------------------------------------------------------
|
||||||
|
|
||||||
def _lire_fichier_contexte(fichier: Path, dossier: Path) -> Ecosysteme:
|
def _lire_fichier_contexte(fichier: Path, dossier: Path) -> Ecosysteme:
|
||||||
|
|
@ -343,7 +531,30 @@ def actif(racine: Path = RACINE, dossier: Path | None = None) -> Ecosysteme | No
|
||||||
def main(argv: list[str] | None = None) -> int:
|
def main(argv: list[str] | None = None) -> int:
|
||||||
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||||
ap.add_argument("--json", action="store_true")
|
ap.add_argument("--json", action="store_true")
|
||||||
|
ap.add_argument("--fiche", metavar="LOCATAIRE", help="la fiche que son hebergeur lui destine")
|
||||||
|
ap.add_argument("--verifier-fiches", action="store_true",
|
||||||
|
help="chaque fiche dit-elle ce que le locataire porte ?")
|
||||||
args = ap.parse_args(argv)
|
args = ap.parse_args(argv)
|
||||||
|
if args.fiche:
|
||||||
|
loc = Locataire.charger(args.fiche)
|
||||||
|
site = loc.site()
|
||||||
|
if site is None:
|
||||||
|
print(f"{loc.nom} ne nomme aucun hebergeur.")
|
||||||
|
return 1
|
||||||
|
print(yaml.safe_dump(site.fiche_pour(loc), allow_unicode=True, sort_keys=False), end="")
|
||||||
|
return 0
|
||||||
|
if args.verifier_fiches:
|
||||||
|
couples = couples_hebergeur()
|
||||||
|
if not couples:
|
||||||
|
print("Aucun site frere : aucune fiche a verifier.")
|
||||||
|
return 0
|
||||||
|
total = 0
|
||||||
|
for site, loc in couples:
|
||||||
|
e = verifier_fiche(site, loc)
|
||||||
|
total += len(e)
|
||||||
|
print(f"{'OK ' if not e else 'ECART'} {site.nom} -> {loc.nom}"
|
||||||
|
+ "".join(f"\n - {x}" for x in e))
|
||||||
|
return 1 if total else 0
|
||||||
try:
|
try:
|
||||||
eco = actif()
|
eco = actif()
|
||||||
erreur = None
|
erreur = None
|
||||||
|
|
|
||||||
|
|
@ -4665,6 +4665,33 @@ def preuve_runbooks_suivent_le_makefile() -> tuple[bool, str]:
|
||||||
f"chacune portee par un assistant ou exemptee avec son motif.")
|
f"chacune portee par un assistant ou exemptee avec son motif.")
|
||||||
|
|
||||||
|
|
||||||
|
def preuve_fiche_du_site() -> tuple[bool, str]:
|
||||||
|
"""La fiche que chaque site destine a ses locataires dit-elle ce qu'ils portent deja ?
|
||||||
|
|
||||||
|
POURQUOI (2026-10-04, `docs/conception-contextes.md` §2.4). Le site informait le locataire
|
||||||
|
par trois canaux, sans fiche : des copies ecrites a la main (index, adresse publique,
|
||||||
|
intrants, racine), une lecture DIRECTE du site par l'instancier (temps, delegation DNS,
|
||||||
|
routage SDN), et le genome. La fiche les reunit. Avant qu'un seul consommateur bascule sur
|
||||||
|
elle (etape 3), elle doit dire EXACTEMENT ce que ces canaux ont depose : sinon la bascule
|
||||||
|
changerait la flotte au lieu de la decrire.
|
||||||
|
|
||||||
|
Pour chaque couple (site, locataire) dont le site est l'hebergeur ACTIF : ecarts entre
|
||||||
|
`site.fiche_pour(locataire)` et les copies du locataire, sa racine, et son inventaire
|
||||||
|
genere. `scripts/tests/test_contexte.py` prouve que chaque sorte d'ecart est vue.
|
||||||
|
"""
|
||||||
|
sys.path.insert(0, str(RACINE / "scripts"))
|
||||||
|
import contexte as _ctx
|
||||||
|
couples = _ctx.couples_hebergeur()
|
||||||
|
if not couples:
|
||||||
|
return True, "Aucun site frere : aucune fiche a confronter."
|
||||||
|
ecarts = [f"{s.nom} -> {l.nom} : {e}" for s, l in couples for e in _ctx.verifier_fiche(s, l)]
|
||||||
|
if ecarts:
|
||||||
|
return False, (f"{len(ecarts)} ecart(s) entre la fiche du site et ce que le locataire "
|
||||||
|
f"porte : " + " | ".join(ecarts[:4]))
|
||||||
|
return True, (f"{len(couples)} fiche(s) de site : chacune dit exactement ce que son locataire "
|
||||||
|
f"porte (copies, racine, inventaire genere).")
|
||||||
|
|
||||||
|
|
||||||
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"]]},
|
||||||
|
|
@ -4851,6 +4878,8 @@ PREUVES: list[dict] = [
|
||||||
"refs": [], "func": preuve_dns_public_tient},
|
"refs": [], "func": preuve_dns_public_tient},
|
||||||
{"id": "P83", "titre": "Assistants : le registre des runbooks ne prend pas de retard sur le Makefile",
|
{"id": "P83", "titre": "Assistants : le registre des runbooks ne prend pas de retard sur le Makefile",
|
||||||
"refs": [], "func": preuve_runbooks_suivent_le_makefile},
|
"refs": [], "func": preuve_runbooks_suivent_le_makefile},
|
||||||
|
{"id": "P84", "titre": "La fiche du site dit exactement ce que chaque locataire porte",
|
||||||
|
"refs": [], "func": preuve_fiche_du_site},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,8 @@ Sur une arborescence FABRIQUEE (un site, deux locataires, un depot ambigu), on v
|
||||||
locataire l'emporte quand les deux sont montes (le poste d'aujourd'hui) ;
|
locataire l'emporte quand les deux sont montes (le poste d'aujourd'hui) ;
|
||||||
- un contexte nomme qui ne designe rien est un REFUS, pas un contexte vide.
|
- un contexte nomme qui ne designe rien est un REFUS, pas un contexte vide.
|
||||||
Puis, si les vrais depots sont la : chaque locataire declare par un site porte l'index que ce
|
Puis, si les vrais depots sont la : chaque locataire declare par un site porte l'index que ce
|
||||||
site lui attribue, et nomme un hebergeur qui existe.
|
site lui attribue, et nomme un hebergeur qui existe ; la fiche de chaque hebergeur dit
|
||||||
|
exactement ce que son locataire porte, et une fiche alteree (sept facons) est vue en ecart.
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
@ -154,6 +155,49 @@ def vrais_depots() -> None:
|
||||||
f"{site.nom} : aucun locataire declare sans depot ({site.locataires_absents() or 'ok'})")
|
f"{site.nom} : aucun locataire declare sans depot ({site.locataires_absents() or 'ok'})")
|
||||||
|
|
||||||
|
|
||||||
|
def monter_restaure() -> None:
|
||||||
|
avant = {k: os.environ.get(k) for k in ("SETOPS_INSTANCE", "SETOPS_UNDERLAY")}
|
||||||
|
try:
|
||||||
|
with C._monter(C.Site(Path("/nulle/SITE")), C.Locataire(Path("/nulle/OPS"))):
|
||||||
|
dedans = (os.environ.get("SETOPS_UNDERLAY"), os.environ.get("SETOPS_INSTANCE"))
|
||||||
|
raise RuntimeError("sortie par exception")
|
||||||
|
except RuntimeError:
|
||||||
|
pass
|
||||||
|
apres = {k: os.environ.get(k) for k in ("SETOPS_INSTANCE", "SETOPS_UNDERLAY")}
|
||||||
|
verifier(dedans == ("/nulle/SITE/underlay.yml", "/nulle/OPS") and apres == avant,
|
||||||
|
"`_monter` designe le site et le locataire voulus, puis remet l'environnement, meme sur exception")
|
||||||
|
|
||||||
|
|
||||||
|
def fiches() -> None:
|
||||||
|
"""La fiche du site dit ce que chaque locataire porte — et `verifier_fiche` voit chaque ecart."""
|
||||||
|
import copy
|
||||||
|
couples = C.couples_hebergeur()
|
||||||
|
if not couples:
|
||||||
|
print("SAUTE aucun site frere : aucune fiche a confronter")
|
||||||
|
return
|
||||||
|
for site, loc in couples:
|
||||||
|
e = C.verifier_fiche(site, loc)
|
||||||
|
verifier(not e, f"fiche {site.nom} -> {loc.nom} : identique a ce que le locataire porte"
|
||||||
|
+ (f" ({e[0]})" if e else ""))
|
||||||
|
site, loc = couples[0]
|
||||||
|
juste = site.fiche_pour(loc)
|
||||||
|
alterations = {
|
||||||
|
"index": lambda f: f["attribue"].__setitem__("index", (f["attribue"]["index"] or 0) + 1),
|
||||||
|
"adresse publique": lambda f: f["attribue"].__setitem__("ip_publique", "192.0.2.99"),
|
||||||
|
"intrant de service": lambda f: f["offre"].__setitem__("dns_amorcage", "192.0.2.53"),
|
||||||
|
"racine de confiance": lambda f: f["racine_confiance"].__setitem__("sha256", "0" * 64),
|
||||||
|
"serveurs de temps": lambda f: f["generation"].__setitem__("serveurs_temps", ["192.0.2.123"]),
|
||||||
|
"delegation DNS": lambda f: f["generation"].__setitem__("resolveurs_site", ["192.0.2.1"]),
|
||||||
|
"mode de routage": lambda f: f["generation"].__setitem__(
|
||||||
|
"routage_tenants", "switch" if f["generation"]["routage_tenants"] == "sdn" else "sdn"),
|
||||||
|
}
|
||||||
|
for nom, alterer in alterations.items():
|
||||||
|
f = copy.deepcopy(juste)
|
||||||
|
alterer(f)
|
||||||
|
verifier(bool(C.verifier_fiche(site, loc, f)),
|
||||||
|
f"une fiche alteree ({nom}) est vue en ecart")
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
d = Path(tmp)
|
d = Path(tmp)
|
||||||
|
|
@ -161,6 +205,8 @@ def main() -> int:
|
||||||
classes(d)
|
classes(d)
|
||||||
contexte_actif(d, moteur)
|
contexte_actif(d, moteur)
|
||||||
vrais_depots()
|
vrais_depots()
|
||||||
|
monter_restaure()
|
||||||
|
fiches()
|
||||||
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