nom public : la garde ne regardait qu un cote de la cloture
Le plan du site expose observatoire.genese.internal ; serveur_grafana devinait grafana.<domaine>. Grafana fabriquait son root_url, ses liens d alerte et son URL de retour OIDC avec un nom que rien ne sert. C est mot pour mot le defaut du 2026-09-10, quatre jours plus tard, de l autre cote : instancier derive <groupe>_hostname depuis ce jour-la et P67 le garde, mais tous deux ne connaissent que l instance montee — donc un locataire. L inventaire du site n en derivait aucun. Quatre devinettes sur cinq tombaient juste par convention, et c est ce qui rendait le defaut invisible. site_inventaire.py derive les cinq noms du plan. P67 lit maintenant les deux inventaires — celui du site est dynamique, donc elle l EXECUTE au lieu de lire sa source. Eprouvee dans les deux sens : 10 services sur 2 inventaires. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
This commit is contained in:
parent
bb88f1f981
commit
d4bc3e48b7
4 changed files with 186 additions and 29 deletions
64
CHANGELOG.md
64
CHANGELOG.md
|
|
@ -1,5 +1,69 @@
|
|||
# CHANGELOG — Set-OPS
|
||||
|
||||
## 2026-09-14 (17) — La garde regardait un seul cote de la cloture
|
||||
|
||||
L'exploitant, en cherchant son tableau : *« grafana c'etait observatoire.genese.internal »*.
|
||||
Il avait raison, et Grafana ne le savait pas.
|
||||
|
||||
### Le defaut
|
||||
|
||||
Le plan du site expose `observatoire.genese.internal`. `serveur_grafana` devinait
|
||||
`grafana.{{ domaine_interne }}` — sa valeur par defaut. Grafana fabriquait donc son
|
||||
`GF_SERVER_ROOT_URL`, ses liens d'alerte et son URL de retour OIDC avec un nom **que rien
|
||||
ne sert**.
|
||||
|
||||
C'est MOT POUR MOT le defaut du 2026-09-10, quatre jours plus tard, de l'autre cote de la
|
||||
cloture. `instancier` derive `<groupe>_hostname` de l'exposition du plan depuis ce
|
||||
jour-la, et **P67** le garde. Les deux ne connaissent que l'instance MONTEE — donc un
|
||||
locataire. L'inventaire du SITE, lui, n'en derivait aucun :
|
||||
|
||||
| groupe expose | le plan dit | le role devinait |
|
||||
|---|---|---|
|
||||
| `serveur_forgejo` | `forge.genese.internal` | forge… — juste par convention |
|
||||
| `serveur_step_ca` | `pki.genese.internal` | pki… — juste |
|
||||
| `serveur_resolveur` | `dns.genese.internal` | dns… — juste |
|
||||
| `serveur_backup_site` | `sauvegarde.genese.internal` | juste |
|
||||
| **`serveur_grafana`** | **`observatoire.genese.internal`** | **`grafana.…` — faux** |
|
||||
|
||||
Quatre devinettes sur cinq tombaient juste, et c'est precisement ce qui rend ce defaut
|
||||
invisible : *tant que le plan suit la meme convention, la devinette tombe juste et
|
||||
personne ne voit qu'il y a deux sources*. Le seul nom qui s'ecarte de la convention est le
|
||||
seul qui ment.
|
||||
|
||||
### Ce qui est corrige
|
||||
|
||||
`site_inventaire.py` derive `<groupe>_hostname` de l'exposition unique du plan du site —
|
||||
le meme geste, au meme endroit, que chez un locataire. Les cinq noms viennent maintenant
|
||||
du plan.
|
||||
|
||||
**P67 regarde les deux inventaires.** Le site a un inventaire DYNAMIQUE : la preuve
|
||||
l'EXECUTE au lieu de lire un fichier — juger sa source reviendrait a relire le
|
||||
raisonnement au lieu du resultat. Eprouvee dans les deux sens : sans la derivation, elle
|
||||
nomme les cinq services fautifs ; avec, elle en compte **10 sur 2 inventaires**.
|
||||
|
||||
Environment=GF_SERVER_DOMAIN=observatoire.genese.internal
|
||||
Environment=GF_SERVER_ROOT_URL=https://observatoire.genese.internal/
|
||||
|
||||
### La lecon, et elle est generale
|
||||
|
||||
Une garde posee la ou un defaut s'est montre ne couvre pas la ou il peut se montrer aussi.
|
||||
Le site et le locataire ont deux inventaires, et le depot en compte plusieurs autres qui
|
||||
ne regardent qu'un des deux. C'est la meme forme que « une liste qui suit une autre prend
|
||||
du retard », appliquee aux gardes elles-memes.
|
||||
|
||||
### Et la console etait joignable depuis le debut
|
||||
|
||||
Le poste porte `10.37.0.17/24`, et nftables accepte le port 3000 depuis `10.37.0.0/24`. Il
|
||||
manquait une ROUTE : sans elle, le noyau envoyait les paquets a la passerelle du LAN avec
|
||||
l'adresse du LAN comme source, et le pare-feu du site voyait arriver un inconnu. Forcer la
|
||||
source ne sert a rien — c'est la route qui la choisit.
|
||||
|
||||
Les routes du poste pointaient aussi les deux locataires vers `10.17.0.1`, une passerelle
|
||||
que **la frontiere ne porte pas** (elle route les tenants par `10.0.4.41` sur le vlan040).
|
||||
Cinq routes vers un routeur inexistant, dont les `/16` des deux ecosystemes : c'est ce qui
|
||||
donnait « No route to host » en cherchant la console de Chezlepro. Routes refaites par
|
||||
l'exploitant ; les trois runners repondent.
|
||||
|
||||
## 2026-09-14 (16) — Le dernier maillon : l'exportateur PostgreSQL, et huit panneaux qui repondent
|
||||
|
||||
L'entree precedente laissait le tableau de PostgreSQL assemble, charge, et **vide** :
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@
|
|||
| P64 | Sondes de supervision : declarees ET deposees | — | ✅ OK | 39 sonde(s) declaree(s) ET deposee(s), chacune avec sa raison et son `ttl` : client_journal/journaux, client_metrique/metriques, client_pki/certificat, serveur_ |
|
||||
| P65 | Depots tiers : demandes au cache, jamais en HTTPS direct | — | ✅ OK | 4 depot(s) tiers relaye(s) par le cache, aucun role ne les vise en https:// ecrit en dur. |
|
||||
| P66 | Clients OIDC : chaque URI vise un nom que le plan expose | — | ✅ OK | 3 client(s) OIDC, toutes leurs URI visent un FQDN que le plan expose (5 exposition(s)). |
|
||||
| P67 | Nom public : le service porte celui du plan, pas celui du role | — | ✅ OK | 5 service(s) expose(s) portent le nom du plan (5 groupe(s) derive(s)). |
|
||||
| P67 | Nom public : le service porte celui du plan, pas celui du role | — | ✅ OK | 10 service(s) expose(s) portent le nom du plan, sur 2 inventaire(s) : instance, SITE. |
|
||||
| P68 | Cle de depot telechargee : mesuree avant d'etre utilisee | — | ✅ OK | 5 role(s) telechargent une cle de depot, tous la mesurent avant de s'en servir. |
|
||||
| P69 | Amorcage d'un tenant : l'adresse designe le site REEL | — | ✅ OK | 2 adresse(s) d'amorcage designent bien une machine du site. |
|
||||
| P70 | Depot de binaires : il tient tout ce que les roles vont chercher | — | ✅ OK | 6 artefact(s) direct(s) tenus par le depot du site. |
|
||||
|
|
|
|||
|
|
@ -3389,14 +3389,66 @@ def preuve_nom_public_vient_du_plan() -> tuple[bool, str]:
|
|||
Cette preuve garde la derivation : elle refuse qu'un hote serve un groupe expose sans
|
||||
porter le nom du plan. Elle ne juge pas les groupes a deux expositions — aucun nom
|
||||
canonique n'en sort, et le role garde alors la main.
|
||||
|
||||
ELLE REGARDE LES DEUX COTES DE LA CLOTURE DEPUIS LE 2026-09-14, et elle n'en regardait
|
||||
qu'un. Ecrite pour l'instance MONTEE, elle ne voyait que des locataires. L'inventaire
|
||||
du SITE ne derivait donc aucun nom — cinq groupes exposes, cinq devinettes de role.
|
||||
|
||||
Quatre tombaient juste par convention. Le cinquieme non : le plan du site expose
|
||||
`observatoire.genese.internal`, `serveur_grafana` devinait `grafana.…`, et Grafana
|
||||
fabriquait son `root_url` et ses liens avec un nom que rien ne sert. Exactement le
|
||||
defaut du 2026-09-10, quatre jours plus tard, de l'autre cote — parce que la garde
|
||||
avait ete posee la ou il s'etait montre, et pas la ou il pouvait se montrer aussi.
|
||||
"""
|
||||
lots: list[tuple[str, dict, dict]] = []
|
||||
|
||||
inv = RACINE / INV
|
||||
if not inv.is_file():
|
||||
return True, "Aucun inventaire monte : rien a verifier."
|
||||
instance = instance_courante()
|
||||
f_app = instance / "plan" / "applications.yml"
|
||||
if not f_app.is_file():
|
||||
return True, "Aucun plan d'applications : rien a verifier."
|
||||
if inv.is_file():
|
||||
instance = instance_courante()
|
||||
f_app = instance / "plan" / "applications.yml"
|
||||
if f_app.is_file():
|
||||
data = yaml.safe_load(inv.read_text(encoding="utf-8")) or {}
|
||||
enfants = (data.get("all") or {}).get("children") or {}
|
||||
hv: dict = {}
|
||||
for etat in ("hotes_actifs", "hotes_planifies"):
|
||||
for nom, v in ((enfants.get(etat) or {}).get("hosts") or {}).items():
|
||||
hv[nom] = v or {}
|
||||
lots.append((instance.name, _noms_exposes(f_app), {"enfants": enfants, "hv": hv}))
|
||||
|
||||
# LE SITE A UN INVENTAIRE DYNAMIQUE : on le lui demande, on ne lit pas un fichier.
|
||||
site = _plan_du_site_applications()
|
||||
if site:
|
||||
rendu = _inventaire_du_site()
|
||||
if rendu:
|
||||
lots.append(("SITE", _noms_exposes(site), rendu))
|
||||
|
||||
if not lots:
|
||||
return True, "Ni instance montee ni plan de site : rien a verifier."
|
||||
|
||||
fautes: list[str] = []
|
||||
verifies = 0
|
||||
for etiquette, attendu, rendu in lots:
|
||||
enfants, hv = rendu["enfants"], rendu["hv"]
|
||||
for groupe, fqdn in sorted(attendu.items()):
|
||||
for hote in sorted((enfants.get(groupe) or {}).get("hosts") or {}):
|
||||
vu = str((hv.get(hote) or {}).get(f"{groupe}_hostname") or "")
|
||||
verifies += 1
|
||||
if vu != fqdn:
|
||||
fautes.append(f"[{etiquette}] {hote} porte {groupe} sans "
|
||||
f"`{groupe}_hostname` = `{fqdn}` "
|
||||
f"(inventaire : {vu or 'absent'})")
|
||||
if fautes:
|
||||
return False, ("Un service ne porte pas le nom que le plan lui donne — ses URL de "
|
||||
"retour viseront l'ancien :\n - " + "\n - ".join(fautes[:6]))
|
||||
if not verifies:
|
||||
return True, "Aucun groupe expose : rien a deriver."
|
||||
return True, (f"{verifies} service(s) expose(s) portent le nom du plan, "
|
||||
f"sur {len(lots)} inventaire(s) : "
|
||||
+ ", ".join(e for e, _, _ in lots) + ".")
|
||||
|
||||
|
||||
def _noms_exposes(f_app) -> dict:
|
||||
"""Groupe -> le nom unique que le plan lui donne. Deux noms : aucun canonique."""
|
||||
apps = (yaml.safe_load(f_app.read_text(encoding="utf-8")) or {}).get("applications") or {}
|
||||
attendu: dict[str, str] = {}
|
||||
for a in apps.values():
|
||||
|
|
@ -3406,30 +3458,42 @@ def preuve_nom_public_vient_du_plan() -> tuple[bool, str]:
|
|||
g = str(a.get("groupe") or "")
|
||||
if g and len(exp) == 1:
|
||||
attendu[g] = exp[0]
|
||||
if not attendu:
|
||||
return True, "Aucun groupe expose par le plan : rien a deriver."
|
||||
return attendu
|
||||
|
||||
data = yaml.safe_load(inv.read_text(encoding="utf-8")) or {}
|
||||
enfants = (data.get("all") or {}).get("children") or {}
|
||||
hostvars: dict = {}
|
||||
for etat in ("hotes_actifs", "hotes_planifies"):
|
||||
for nom, v in ((enfants.get(etat) or {}).get("hosts") or {}).items():
|
||||
hostvars[nom] = v or {}
|
||||
|
||||
fautes: list[str] = []
|
||||
verifies = 0
|
||||
for groupe, fqdn in sorted(attendu.items()):
|
||||
for hote in sorted((enfants.get(groupe) or {}).get("hosts") or {}):
|
||||
vu = str((hostvars.get(hote) or {}).get(f"{groupe}_hostname") or "")
|
||||
verifies += 1
|
||||
if vu != fqdn:
|
||||
fautes.append(f"{hote} porte {groupe} sans `{groupe}_hostname` = "
|
||||
f"`{fqdn}` (inventaire : {vu or 'absent'})")
|
||||
if fautes:
|
||||
return False, ("Un service ne porte pas le nom que le plan lui donne — ses URL de "
|
||||
"retour viseront l'ancien :\n - " + "\n - ".join(fautes[:6]))
|
||||
return True, (f"{verifies} service(s) expose(s) portent le nom du plan "
|
||||
f"({len(attendu)} groupe(s) derive(s)).")
|
||||
def _plan_du_site_applications():
|
||||
"""Le `applications.yml` du site MONTE, ou None s'il n'y en a pas."""
|
||||
lien = RACINE / "underlay.yml"
|
||||
if not lien.exists():
|
||||
return None
|
||||
f = lien.resolve().parent / "plan" / "applications.yml"
|
||||
return f if f.is_file() else None
|
||||
|
||||
|
||||
def _inventaire_du_site():
|
||||
"""L'inventaire DYNAMIQUE du site, dans la forme que cette preuve consomme.
|
||||
|
||||
On l'EXECUTE au lieu de lire un fichier : c'est un script, et son resultat est la
|
||||
seule chose qu'Ansible verra. Juger sa source reviendrait a relire le raisonnement
|
||||
au lieu du resultat.
|
||||
"""
|
||||
import json
|
||||
import subprocess
|
||||
try:
|
||||
sortie = subprocess.run(
|
||||
[sys.executable, str(RACINE / "scripts" / "site_inventaire.py"), "--list"],
|
||||
capture_output=True, text=True, timeout=120, cwd=RACINE)
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
return None
|
||||
if sortie.returncode != 0:
|
||||
return None
|
||||
try:
|
||||
d = json.loads(sortie.stdout)
|
||||
except ValueError:
|
||||
return None
|
||||
return {"enfants": {g: {"hosts": {h: None for h in (v.get("hosts") or [])}}
|
||||
for g, v in d.items() if g != "_meta"},
|
||||
"hv": (d.get("_meta") or {}).get("hostvars") or {}}
|
||||
|
||||
|
||||
def preuve_cle_telechargee_non_vide() -> tuple[bool, str]:
|
||||
|
|
|
|||
|
|
@ -464,6 +464,31 @@ def inventaire() -> dict:
|
|||
if h.get("role") == "frontiere"
|
||||
and str(h.get("etat") or "actif") == "actif"), "frontiere")
|
||||
|
||||
# LE NOM PUBLIC D'UN SERVICE VIENT DU PLAN, PAS DU ROLE — AUSSI AU SITE (2026-09-14).
|
||||
#
|
||||
# `instancier` derive `<groupe>_hostname` de l'exposition declaree depuis le
|
||||
# 2026-09-10, et P67 le garde. Les deux ne connaissent que l'instance MONTEE, donc un
|
||||
# locataire. L'inventaire du site, lui, n'en derivait AUCUN : ses cinq groupes exposes
|
||||
# tombaient tous sur la devinette de leur role.
|
||||
#
|
||||
# Quatre tombaient juste par convention — `forge`, `pki`, `dns`, `sauvegarde`. Le
|
||||
# cinquieme non : le plan expose `observatoire.genese.internal`, et `serveur_grafana`
|
||||
# devine `grafana.{{ domaine_interne }}`. Grafana fabriquait donc son `root_url`, ses
|
||||
# liens d'alerte et son URL de retour OIDC avec un nom que rien ne sert.
|
||||
#
|
||||
# C'EST LE MEME DEFAUT QU'IL Y A QUATRE JOURS, de l'autre cote de la cloture. La garde
|
||||
# avait ete posee la ou il s'etait montre, et pas la ou il pouvait se montrer aussi.
|
||||
#
|
||||
# UNE SEULE EXPOSITION, sinon rien : deux noms ne designent aucun canonique, et le
|
||||
# role garde alors la main. `setdefault` cote hote : une valeur ecrite a la main dans
|
||||
# les variables d'une machine gagne encore.
|
||||
_hostname_par_groupe: dict = {}
|
||||
for _app in applications.values():
|
||||
_exp = [str(f).strip() for f in (_app.get("expose") or []) if str(f).strip()]
|
||||
_g = str(_app.get("groupe") or "")
|
||||
if _g and len(_exp) == 1:
|
||||
_hostname_par_groupe[_g] = _exp[0]
|
||||
|
||||
hostvars: dict[str, dict] = {}
|
||||
groupes: dict[str, list[str]] = {}
|
||||
|
||||
|
|
@ -652,6 +677,10 @@ def inventaire() -> dict:
|
|||
# plan declare pour toutes.
|
||||
**(srv.get("variables") or {}),
|
||||
}
|
||||
# Nom public de chaque service porte par cet hote, tel que le plan l'expose.
|
||||
for _groupe_expose in sorted(_groupes_de(nom) & set(_hostname_par_groupe)):
|
||||
hostvars[nom].setdefault(f"{_groupe_expose}_hostname",
|
||||
_hostname_par_groupe[_groupe_expose])
|
||||
for _socle in GROUPE_SOCLE:
|
||||
groupes.setdefault(_socle, []).append(nom)
|
||||
if "serveur_backup_site" in _groupes_de(nom):
|
||||
|
|
|
|||
Loading…
Reference in a new issue