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)
|
||||
|
||||
`--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
|
||||
dépôt annonce.**
|
||||
|
||||
|
|
|
|||
39
CHANGELOG.md
39
CHANGELOG.md
|
|
@ -1,5 +1,44 @@
|
|||
# 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
|
||||
|
||||
**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
|
||||
|
||||
`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
|
||||
cohérent **avec lui-même** — que les handlers existent, que les intrants ont un
|
||||
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 :
|
||||
|
||||
```
|
||||
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 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ù
|
||||
|
|
|
|||
|
|
@ -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
|
||||
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
|
||||
scripts l'adopteront un par un, chacun a resultat identique. Les fiches du contrat
|
||||
(`site.fiche_pour`, `locataire.face_reseau`) sont l'etape 2.
|
||||
ETAPES 1 ET 2 DU CHEMIN (§6) : le module existe, il est teste, et RIEN NE L'UTILISE ENCORE.
|
||||
La fiche du site (`site.fiche_pour(locataire)`) est calculee A COTE de l'existant, et
|
||||
`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 :
|
||||
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 --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
|
||||
|
||||
import argparse
|
||||
import contextlib
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
|
@ -53,6 +59,35 @@ def _yaml(chemin: Path) -> dict:
|
|||
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 ----------------------------------------------------------------------
|
||||
|
||||
class Ecosysteme:
|
||||
|
|
@ -210,6 +245,50 @@ class Site(Ecosysteme):
|
|||
presents = {l.nom for l in self.locataires()}
|
||||
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:
|
||||
d = super().decrire()
|
||||
d.update({"locataires": self.noms_locataires(),
|
||||
|
|
@ -258,6 +337,19 @@ class Locataire(Ecosysteme):
|
|||
def voute(self) -> Path:
|
||||
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:
|
||||
h = ((self.filiation.get("depots") or {}).get("hebergeur") or {}).get("nom")
|
||||
return str(h) if h else None
|
||||
|
|
@ -275,6 +367,102 @@ class Locataire(Ecosysteme):
|
|||
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 --------------------------------------------------------------------
|
||||
|
||||
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:
|
||||
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||
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)
|
||||
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:
|
||||
eco = actif()
|
||||
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.")
|
||||
|
||||
|
||||
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] = [
|
||||
{"id": "P01", "titre": "Lint (ansible-lint)", "refs": ["AFF-006"],
|
||||
"cmds": [["ansible-lint", "-q"]]},
|
||||
|
|
@ -4851,6 +4878,8 @@ PREUVES: list[dict] = [
|
|||
"refs": [], "func": preuve_dns_public_tient},
|
||||
{"id": "P83", "titre": "Assistants : le registre des runbooks ne prend pas de retard sur 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) ;
|
||||
- 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
|
||||
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
|
||||
|
||||
|
|
@ -154,6 +155,49 @@ def vrais_depots() -> None:
|
|||
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:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
d = Path(tmp)
|
||||
|
|
@ -161,6 +205,8 @@ def main() -> int:
|
|||
classes(d)
|
||||
contexte_actif(d, moteur)
|
||||
vrais_depots()
|
||||
monter_restaure()
|
||||
fiches()
|
||||
if ECHECS:
|
||||
print(f"\n{len(ECHECS)} echec(s).")
|
||||
return 1
|
||||
|
|
|
|||
Loading…
Reference in a new issue