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
|
# 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
|
## 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** :
|
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_ |
|
| 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. |
|
| 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)). |
|
| 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. |
|
| 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. |
|
| 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. |
|
| 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
|
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
|
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.
|
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
|
inv = RACINE / INV
|
||||||
if not inv.is_file():
|
if inv.is_file():
|
||||||
return True, "Aucun inventaire monte : rien a verifier."
|
instance = instance_courante()
|
||||||
instance = instance_courante()
|
f_app = instance / "plan" / "applications.yml"
|
||||||
f_app = instance / "plan" / "applications.yml"
|
if f_app.is_file():
|
||||||
if not f_app.is_file():
|
data = yaml.safe_load(inv.read_text(encoding="utf-8")) or {}
|
||||||
return True, "Aucun plan d'applications : rien a verifier."
|
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 {}
|
apps = (yaml.safe_load(f_app.read_text(encoding="utf-8")) or {}).get("applications") or {}
|
||||||
attendu: dict[str, str] = {}
|
attendu: dict[str, str] = {}
|
||||||
for a in apps.values():
|
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 "")
|
g = str(a.get("groupe") or "")
|
||||||
if g and len(exp) == 1:
|
if g and len(exp) == 1:
|
||||||
attendu[g] = exp[0]
|
attendu[g] = exp[0]
|
||||||
if not attendu:
|
return attendu
|
||||||
return True, "Aucun groupe expose par le plan : rien a deriver."
|
|
||||||
|
|
||||||
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] = []
|
def _plan_du_site_applications():
|
||||||
verifies = 0
|
"""Le `applications.yml` du site MONTE, ou None s'il n'y en a pas."""
|
||||||
for groupe, fqdn in sorted(attendu.items()):
|
lien = RACINE / "underlay.yml"
|
||||||
for hote in sorted((enfants.get(groupe) or {}).get("hosts") or {}):
|
if not lien.exists():
|
||||||
vu = str((hostvars.get(hote) or {}).get(f"{groupe}_hostname") or "")
|
return None
|
||||||
verifies += 1
|
f = lien.resolve().parent / "plan" / "applications.yml"
|
||||||
if vu != fqdn:
|
return f if f.is_file() else None
|
||||||
fautes.append(f"{hote} porte {groupe} sans `{groupe}_hostname` = "
|
|
||||||
f"`{fqdn}` (inventaire : {vu or 'absent'})")
|
|
||||||
if fautes:
|
def _inventaire_du_site():
|
||||||
return False, ("Un service ne porte pas le nom que le plan lui donne — ses URL de "
|
"""L'inventaire DYNAMIQUE du site, dans la forme que cette preuve consomme.
|
||||||
"retour viseront l'ancien :\n - " + "\n - ".join(fautes[:6]))
|
|
||||||
return True, (f"{verifies} service(s) expose(s) portent le nom du plan "
|
On l'EXECUTE au lieu de lire un fichier : c'est un script, et son resultat est la
|
||||||
f"({len(attendu)} groupe(s) derive(s)).")
|
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]:
|
def preuve_cle_telechargee_non_vide() -> tuple[bool, str]:
|
||||||
|
|
|
||||||
|
|
@ -464,6 +464,31 @@ def inventaire() -> dict:
|
||||||
if h.get("role") == "frontiere"
|
if h.get("role") == "frontiere"
|
||||||
and str(h.get("etat") or "actif") == "actif"), "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] = {}
|
hostvars: dict[str, dict] = {}
|
||||||
groupes: dict[str, list[str]] = {}
|
groupes: dict[str, list[str]] = {}
|
||||||
|
|
||||||
|
|
@ -652,6 +677,10 @@ def inventaire() -> dict:
|
||||||
# plan declare pour toutes.
|
# plan declare pour toutes.
|
||||||
**(srv.get("variables") or {}),
|
**(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:
|
for _socle in GROUPE_SOCLE:
|
||||||
groupes.setdefault(_socle, []).append(nom)
|
groupes.setdefault(_socle, []).append(nom)
|
||||||
if "serveur_backup_site" in _groupes_de(nom):
|
if "serveur_backup_site" in _groupes_de(nom):
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue