console chez les locataires, collision de noms, et un fichier ecrase
Les deux locataires declarent leur console derriere oauth2-proxy. En oidc le vestibule n ecoute que la boucle locale : le gabarit annoncait cette protection en s en remettant au pare-feu, qui ouvrait le port depuis l edge — la console etait joignable sans passer par la passerelle. COLLISION REELLE : serveur_oauth2_proxy est mono-instance par machine, et la table des noms publics etait indexee par GROUPE. La passerelle de la vigie se croyait la console ; son URL de retour OIDC aurait vise l autre machine. Une application nomme un COUPLE (machine, role). P67 ne le voyait pas parce qu elle indexait comme la derivation qu elle garde — une garde qui reproduit le raisonnement qu elle verifie ne verifie rien. ET J AI ECRASE group_vars/serveur_ops.yml sans le regarder : 68 lignes detruites, deux preuves tombees, et une explication fausse construite dessus. Le git diff qui m avait rassure portait un glob developpe par le shell parent : il n a rien matche et n a rien dit. Une commande qui ne trouve rien et une commande qui trouve que rien n a change rendent le meme silence. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
This commit is contained in:
parent
0529951381
commit
e93e8f4c1d
8 changed files with 180 additions and 39 deletions
68
CHANGELOG.md
68
CHANGELOG.md
|
|
@ -1,5 +1,73 @@
|
||||||
# CHANGELOG — Set-OPS
|
# CHANGELOG — Set-OPS
|
||||||
|
|
||||||
|
## 2026-09-15 (5) — La console chez les locataires, une collision de noms, et un fichier que j'ai ecrase
|
||||||
|
|
||||||
|
Les deux locataires declarent desormais leur console derriere leur SSO. Le chemin a
|
||||||
|
traverse une collision reelle et une faute de ma part.
|
||||||
|
|
||||||
|
### `console.<domaine>`, derriere oauth2-proxy
|
||||||
|
|
||||||
|
ops-01 serveur_ops_gui_actif: true, auth: oidc
|
||||||
|
le GUI sur 127.0.0.1:8765, le vestibule nginx sur 127.0.0.1:8090
|
||||||
|
la passerelle sur 4180, publiee par l'edge
|
||||||
|
|
||||||
|
`oidc` ET PAS `locale` : ces ecosystemes ont un annuaire. Cette console lance des
|
||||||
|
deploiements et peut RASER — un groupe se revoque sans deploiement (D-66), un mot de passe
|
||||||
|
partage devant ce pouvoir est un accident qui attend.
|
||||||
|
|
||||||
|
ET LE VESTIBULE N'ECOUTE PLUS QUE LA BOUCLE LOCALE EN `oidc`. Le gabarit annoncait cette
|
||||||
|
protection — *« lui seul doit pouvoir frapper ce port »* — en s'en remettant a
|
||||||
|
`meta/flux.yml`, qui ouvre le 8090 depuis `[edge, admin]`. L'edge pouvait donc joindre la
|
||||||
|
console SANS passer par la passerelle. Un port qui n'est pas ouvert ne se contourne pas ;
|
||||||
|
une regle, si.
|
||||||
|
|
||||||
|
### UNE COLLISION DE NOMS QUE LA GARDE NE VOYAIT PAS
|
||||||
|
|
||||||
|
`serveur_oauth2_proxy` est mono-instance par machine : une devant la vigie sur le noeud de
|
||||||
|
supervision, une devant la console sur le runner. La table des noms publics etait indexee
|
||||||
|
par GROUPE :
|
||||||
|
|
||||||
|
mon-01 serveur_oauth2_proxy_hostname = console.chezlepro.internal
|
||||||
|
ops-01 serveur_oauth2_proxy_hostname = console.chezlepro.internal
|
||||||
|
|
||||||
|
La passerelle de la VIGIE se croyait la console. Son URL de retour OIDC aurait vise l'autre
|
||||||
|
machine, et le SSO de la supervision serait tombe — pour un service auquel on n'avait pas
|
||||||
|
touche.
|
||||||
|
|
||||||
|
**Une application nomme un COUPLE (machine, role).** `instancier` et `site_inventaire` en
|
||||||
|
ont la clef juste desormais.
|
||||||
|
|
||||||
|
ET P67 NE LE VOYAIT PAS, parce qu'elle indexait par groupe **comme la derivation qu'elle
|
||||||
|
garde** : elle comparait la valeur a elle-meme. *Une garde qui reproduit le raisonnement
|
||||||
|
qu'elle verifie ne verifie rien.* Elle compare maintenant par machine, et elle a ete
|
||||||
|
eprouvee en lui remettant la faute sous les yeux.
|
||||||
|
|
||||||
|
### CE QUE J'AI CASSE, ET LA MESURE QUI ME L'A CACHE
|
||||||
|
|
||||||
|
J'ai ecrit `group_vars/serveur_ops.yml` avec `cat >` **sans regarder s'il existait**. Il
|
||||||
|
existait : 84 lignes chez Chezlepro, 77 chez TechnoLibre. J'en ai detruit 68 — la liste
|
||||||
|
des depots du genome, la branche `master` de TechnoLibre, l'amont de la forge, la source
|
||||||
|
de la cle de voute.
|
||||||
|
|
||||||
|
Deux preuves sont tombees (P05, P32). **J'ai d'abord accuse le retrait des forges de la
|
||||||
|
veille**, et construit une explication complete : « le site expose l'amont, le locataire
|
||||||
|
ne le porte pas ». Elle etait fausse — le fichier restaure le portait deja.
|
||||||
|
|
||||||
|
LA MESURE QUI M'A RASSURE A TORT :
|
||||||
|
|
||||||
|
(cd $e && git diff -- inventories/*/group_vars/serveur_ops.yml)
|
||||||
|
|
||||||
|
Le glob est developpe par le shell PARENT, ou `inventories/` n'existe pas. Le motif est
|
||||||
|
passe tel quel, n'a rien matche, et `git diff` n'a rien affiche. J'ai lu ce silence comme
|
||||||
|
« aucun fichier ecrase ». C'est `git status` — sans glob — qui a fini par montrer le `M`.
|
||||||
|
|
||||||
|
Une commande qui ne trouve rien et une commande qui trouve que rien n'a change rendent le
|
||||||
|
meme silence. Encore la meme forme que les six replis des deux derniers jours, cette
|
||||||
|
fois dans mes propres mains.
|
||||||
|
|
||||||
|
Restaure par `git checkout`, puis les trois lignes de la console ajoutees SANS toucher au
|
||||||
|
reste. Harnais : **77 OK**.
|
||||||
|
|
||||||
## 2026-09-15 (4) — La console d'exploitation est allumee, et publiee par l'edge
|
## 2026-09-15 (4) — La console d'exploitation est allumee, et publiee par l'edge
|
||||||
|
|
||||||
https://console.genese.internal/ 401 sans justificatif, la console avec
|
https://console.genese.internal/ 401 sans justificatif, la console avec
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@
|
||||||
| P15 | Modele public socle valide | AFF-022, AFF-099 | ✅ OK | Modele public socle : domaines/serveurs/applications/bases valides. |
|
| P15 | Modele public socle valide | AFF-022, AFF-099 | ✅ OK | Modele public socle : domaines/serveurs/applications/bases valides. |
|
||||||
| P16 | Inventaire Ansible complet (--list) | AFF-030 | ⚪ SAUTE | Voute chiffree sans ANSIBLE_VAULT_PASSWORD_FILE (prerequis AFF-026). |
|
| P16 | Inventaire Ansible complet (--list) | AFF-030 | ⚪ SAUTE | Voute chiffree sans ANSIBLE_VAULT_PASSWORD_FILE (prerequis AFF-026). |
|
||||||
| P17 | Tous les modeles valident (registres + underlay) | AFF-022, AFF-099 | ✅ OK | Les 1 modele(s) decouvert(s) valident. |
|
| P17 | Tous les modeles valident (registres + underlay) | AFF-022, AFF-099 | ✅ OK | Les 1 modele(s) decouvert(s) valident. |
|
||||||
| P18 | Gabarit de voute complet | AFF-026 | ✅ OK | Gabarit de voute complet : 30 secret(s) exige(s), tous presents. (Voute reelle non lisible ici : verification sautee.) |
|
| P18 | Gabarit de voute complet | AFF-026 | ✅ OK | Gabarit de voute complet : 31 secret(s) exige(s), tous presents. (Voute reelle non lisible ici : verification sautee.) |
|
||||||
| P19 | Le GUI couvre le schema du plan | AFF-002, AFF-095 | ✅ OK | GUI : les 29 champ(s) des plans reels sont editables (2 plan(s) inspecte(s)), registres toleres : nomenclature. |
|
| P19 | Le GUI couvre le schema du plan | AFF-002, AFF-095 | ✅ OK | GUI : les 29 champ(s) des plans reels sont editables (2 plan(s) inspecte(s)), registres toleres : nomenclature. |
|
||||||
| P20 | Adressage 100% derive du seed (aucun stocke) | AFF-001, AFF-003 | ✅ OK | 2 nomenclature(s) : adressage 100% derive du seed index. |
|
| P20 | Adressage 100% derive du seed (aucun stocke) | AFF-001, AFF-003 | ✅ OK | 2 nomenclature(s) : adressage 100% derive du seed index. |
|
||||||
| P21 | Federation : aucun index en collision | AFF-102 | ✅ OK | Federation coherente : 6 instance(s) federee(s), aucun index en collision. |
|
| P21 | Federation : aucun index en collision | AFF-102 | ✅ OK | Federation coherente : 6 instance(s) federee(s), aucun index en collision. |
|
||||||
|
|
@ -44,7 +44,7 @@
|
||||||
| P29 | Authentification : chaque role declare sa position | AFF-111 | ✅ OK | 33 role(s) serveur declares (interne-sans-auth 2, ldap-direct 2, sans-auth-humaine 22, socle-identite 2, web-sso 5) ; 2 lacune(s) nommee(s) : serveur_loki, serv |
|
| P29 | Authentification : chaque role declare sa position | AFF-111 | ✅ OK | 33 role(s) serveur declares (interne-sans-auth 2, ldap-direct 2, sans-auth-humaine 22, socle-identite 2, web-sso 5) ; 2 lacune(s) nommee(s) : serveur_loki, serv |
|
||||||
| P30 | SDN EVPN : zones, VNets et sous-reseaux derives | AFF-112 | ✅ OK | CONFORME : SDN EVPN, 3 zone(s), 15 VNet(s), 15 sous-reseau(x), aucune collision. |
|
| P30 | SDN EVPN : zones, VNets et sous-reseaux derives | AFF-112 | ✅ OK | CONFORME : SDN EVPN, 3 zone(s), 15 VNet(s), 15 sous-reseau(x), aucune collision. |
|
||||||
| P31 | Documentation : tout ce que le depot FAIT est nomme | — | ✅ OK | 67 scripts expliques et atteignables, 123 cibles make documentees, 68 roles avec README. |
|
| P31 | Documentation : tout ce que le depot FAIT est nomme | — | ✅ OK | 67 scripts expliques et atteignables, 123 cibles make documentees, 68 roles avec README. |
|
||||||
| P32 | Intrants exiges par les roles : tous fournis | — | ✅ OK | CONFORME : 36 exigence(s) de role, toutes satisfaites (142 cle(s) declaree(s) par l'instance). |
|
| P32 | Intrants exiges par les roles : tous fournis | — | ✅ OK | CONFORME : 36 exigence(s) de role, toutes satisfaites (144 cle(s) declaree(s) par l'instance). |
|
||||||
| P33 | Aucune collision de port entre roles co-localises | — | ✅ OK | CONFORME : 36 revendication(s) de port, aucune collision entre roles co-localises (35 groupes). |
|
| P33 | Aucune collision de port entre roles co-localises | — | ✅ OK | CONFORME : 36 revendication(s) de port, aucune collision entre roles co-localises (35 groupes). |
|
||||||
| P34 | Chaque document declare son lecteur | — | ✅ OK | 46 document(s) declarent leur lecteur (41 genere(s) exempte(s)). |
|
| P34 | Chaque document declare son lecteur | — | ✅ OK | 46 document(s) declarent leur lecteur (41 genere(s) exempte(s)). |
|
||||||
| P35 | Toute application exigeant une base en a une au plan | — | ✅ OK | 4 application(s) exigeant une base l'ont toutes (3 entree(s) au registre). |
|
| P35 | Toute application exigeant une base en a une au plan | — | ✅ OK | 4 application(s) exigeant une base l'ont toutes (3 entree(s) au registre). |
|
||||||
|
|
@ -78,8 +78,8 @@
|
||||||
| P63 | cloud-init nait avec la VM et ne lui survit pas | — | ✅ OK | cloud-init est au gabarit (la premiere seconde), absent du socle (pas de va-et-vient), et retire par le durcissement — avec la garde qui verifie que le reseau s |
|
| P63 | cloud-init nait avec la VM et ne lui survit pas | — | ✅ OK | cloud-init est au gabarit (la premiere seconde), absent du socle (pas de va-et-vient), et retire par le durcissement — avec la garde qui verifie que le reseau s |
|
||||||
| P64 | Sondes de supervision : declarees ET deposees | — | ✅ OK | 40 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 | 40 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 | 4 client(s) OIDC, toutes leurs URI visent un FQDN que le plan expose (6 exposition(s)). |
|
||||||
| P67 | Nom public : le service porte celui du plan, pas celui du role | — | ✅ OK | 12 service(s) expose(s) portent le nom du plan, sur 2 inventaire(s) : instance, SITE. |
|
| P67 | Nom public : le service porte celui du plan, pas celui du role | — | ✅ OK | 13 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. |
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,14 @@ vault_icingaweb2_admin: ""
|
||||||
# accident qui attend.
|
# accident qui attend.
|
||||||
vault_setops_gui_admin: ""
|
vault_setops_gui_admin: ""
|
||||||
|
|
||||||
|
# LE SECRET OIDC DE LA CONSOLE D'EXPLOITATION (mode `oidc`).
|
||||||
|
#
|
||||||
|
# Le GUI de Set-OPS n'a aucune authentification a lui : la passerelle est sa seule
|
||||||
|
# serrure, et ce secret est ce qui la lie a Keycloak. Vide chez un ecosysteme sans
|
||||||
|
# annuaire — un SITE — qui emploie alors le vestibule local.
|
||||||
|
vault_setops_console_oidc: ""
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# LE COMPTE DE METRIQUES DE POSTGRESQL — lecture seule, role `pg_monitor`.
|
# LE COMPTE DE METRIQUES DE POSTGRESQL — lecture seule, role `pg_monitor`.
|
||||||
#
|
#
|
||||||
|
|
|
||||||
|
|
@ -240,6 +240,22 @@ serveur_ops_gui_port: 8765
|
||||||
# LE PORT QUE L'EDGE VA CHERCHER. C'est nginx qui l'ouvre, jamais le GUI lui-meme.
|
# LE PORT QUE L'EDGE VA CHERCHER. C'est nginx qui l'ouvre, jamais le GUI lui-meme.
|
||||||
serveur_ops_gui_port_public: 8090
|
serveur_ops_gui_port_public: 8090
|
||||||
|
|
||||||
|
# SUR QUOI CE PORT S'OUVRE — ET CA DEPEND DU VESTIBULE (2026-09-15).
|
||||||
|
#
|
||||||
|
# En `locale`, nginx EST le vestibule : il doit etre joignable par l'edge, donc il ecoute
|
||||||
|
# sur toutes les interfaces et `meta/flux.yml` ouvre le 8090 depuis `[edge, admin]`.
|
||||||
|
#
|
||||||
|
# En `oidc`, la passerelle SSO se tient devant et c'est ELLE qui est publiee. Si nginx
|
||||||
|
# restait ouvert, l'edge pourrait frapper le 8090 directement et CONTOURNER la passerelle
|
||||||
|
# — la console serait alors accessible sans authentification a qui atteint l'hote. Le
|
||||||
|
# commentaire du gabarit annoncait cette protection ; rien ne la tenait.
|
||||||
|
#
|
||||||
|
# On la tient ici, a l'ecoute, et pas par une regle : un port qui n'est pas ouvert ne se
|
||||||
|
# contourne pas, alors qu'une regle se deplace. La regle du 8090 reste declaree pour le
|
||||||
|
# mode `locale`, et devient simplement sans objet en `oidc`.
|
||||||
|
serveur_ops_gui_nginx_bind: >-
|
||||||
|
{{ '127.0.0.1:' if serveur_ops_gui_auth == 'oidc' else '' }}
|
||||||
|
|
||||||
# DEUX VESTIBULES, ET LE PREMIER EST LE BON.
|
# DEUX VESTIBULES, ET LE PREMIER EST LE BON.
|
||||||
#
|
#
|
||||||
# `oidc` — `serveur_oauth2_proxy` devant, donc Keycloak, donc un GROUPE d'annuaire.
|
# `oidc` — `serveur_oauth2_proxy` devant, donc Keycloak, donc un GROUPE d'annuaire.
|
||||||
|
|
@ -268,9 +284,19 @@ serveur_ops_gui_admin_motdepasse: ""
|
||||||
serveur_ops_gui_htpasswd: "/etc/nginx/.setops-gui.htpasswd"
|
serveur_ops_gui_htpasswd: "/etc/nginx/.setops-gui.htpasswd"
|
||||||
serveur_ops_gui_realm: "Console Set-OPS"
|
serveur_ops_gui_realm: "Console Set-OPS"
|
||||||
|
|
||||||
# LE NOM SOUS LEQUEL L'EDGE LE SERT — derive du plan par `instancier` et par
|
# LE NOM SOUS LEQUEL LA CONSOLE EST SERVIE.
|
||||||
# `site_inventaire`, comme tout nom public (P67). Le defaut n'est qu'un repli.
|
#
|
||||||
serveur_ops_hostname: "console.{{ domaine_interne }}"
|
# EN `oidc`, LE PLAN LE DONNE A LA PASSERELLE, PAS AU RUNNER. C'est elle qui est publiee ;
|
||||||
|
# `serveur_ops_hostname` sert alors au `server_name` du vestibule local, qui doit
|
||||||
|
# reconnaitre l'en-tete `Host` que la passerelle relaie telle quelle. On le derive donc du
|
||||||
|
# nom de la passerelle POSEE SUR CETTE MACHINE — une seule source, pas deux conventions.
|
||||||
|
#
|
||||||
|
# EN `locale`, le plan expose le runner lui-meme et `instancier` pose directement
|
||||||
|
# `serveur_ops_hostname` : ce `default` ne s'applique alors pas.
|
||||||
|
#
|
||||||
|
# Le dernier repli garde la convention du depot, pour un ecosysteme qui n'expose rien.
|
||||||
|
serveur_ops_hostname: >-
|
||||||
|
{{ serveur_oauth2_proxy_hostname | default('console.' ~ domaine_interne, true) }}
|
||||||
|
|
||||||
# Le vestibule et de quoi hacher son mot de passe.
|
# Le vestibule et de quoi hacher son mot de passe.
|
||||||
serveur_ops_gui_paquets:
|
serveur_ops_gui_paquets:
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,10 @@
|
||||||
# passer tout chemin qu'un `location` plus specifique attraperait en premier. Une
|
# passer tout chemin qu'un `location` plus specifique attraperait en premier. Une
|
||||||
# serrure qu'on contourne par la porte d'a cote n'est pas une serrure.
|
# serrure qu'on contourne par la porte d'a cote n'est pas une serrure.
|
||||||
server {
|
server {
|
||||||
listen {{ serveur_ops_gui_port_public }};
|
listen {{ serveur_ops_gui_nginx_bind }}{{ serveur_ops_gui_port_public }};
|
||||||
|
{% if not serveur_ops_gui_nginx_bind %}
|
||||||
listen [::]:{{ serveur_ops_gui_port_public }};
|
listen [::]:{{ serveur_ops_gui_port_public }};
|
||||||
|
{% endif %}
|
||||||
server_name {{ serveur_ops_hostname }};
|
server_name {{ serveur_ops_hostname }};
|
||||||
{% if serveur_ops_gui_auth == 'locale' %}
|
{% if serveur_ops_gui_auth == 'locale' %}
|
||||||
|
|
||||||
|
|
@ -18,10 +20,13 @@ server {
|
||||||
auth_basic_user_file {{ serveur_ops_gui_htpasswd }};
|
auth_basic_user_file {{ serveur_ops_gui_htpasswd }};
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
||||||
# AVEC ANNUAIRE : c'est `serveur_oauth2_proxy` qui se tient devant, et lui seul doit
|
# AVEC ANNUAIRE : `serveur_oauth2_proxy` se tient devant, sur la MEME machine, et
|
||||||
# pouvoir frapper ce port. Le pare-feu de l'hote le dit deja (`meta/flux.yml`), et on
|
# c'est lui qui est publie. Ce vhost n'ecoute alors que la boucle locale
|
||||||
# ne le redit pas ici : deux couches qui declarent la meme politique finissent par
|
# (`serveur_ops_gui_nginx_bind`) — un port qui n'est pas ouvert ne se contourne pas.
|
||||||
# en declarer deux differentes.
|
#
|
||||||
|
# LA PREMIERE ECRITURE S'EN REMETTAIT AU PARE-FEU, et le pare-feu ouvrait ce port
|
||||||
|
# depuis `[edge, admin]` : l'edge pouvait donc joindre la console SANS passer par la
|
||||||
|
# passerelle. Le commentaire annoncait une protection que rien ne tenait.
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
# LA CONSOLE DEPLOIE, ET UN DEPLOIEMENT EST LONG. Les delais par defaut de nginx
|
# LA CONSOLE DEPLOIE, ET UN DEPLOIEMENT EST LONG. Les delais par defaut de nginx
|
||||||
|
|
|
||||||
|
|
@ -211,12 +211,31 @@ def generer() -> dict:
|
||||||
# On derive donc `<groupe>_hostname` de l'exposition declaree, quand elle est UNIQUE
|
# On derive donc `<groupe>_hostname` de l'exposition declaree, quand elle est UNIQUE
|
||||||
# (deux expositions ne designent aucun nom canonique : on laisse le role choisir).
|
# (deux expositions ne designent aucun nom canonique : on laisse le role choisir).
|
||||||
# `setdefault` cote hote : une valeur posee a la main dans les group_vars gagne encore.
|
# `setdefault` cote hote : une valeur posee a la main dans les group_vars gagne encore.
|
||||||
hostname_par_groupe: dict = {}
|
# PAR HOTE *ET* PAR GROUPE, PAS PAR GROUPE SEUL (2026-09-15).
|
||||||
|
#
|
||||||
|
# Un ROLE peut servir DEUX applications sur DEUX machines. `serveur_oauth2_proxy` en
|
||||||
|
# est l'exemple : une instance devant la vigie sur le noeud de supervision, une autre
|
||||||
|
# devant la console sur le runner. Deux passerelles, deux clients Keycloak, deux noms.
|
||||||
|
#
|
||||||
|
# Indexee par groupe seul, la table gardait le DERNIER nom rencontre et le donnait a
|
||||||
|
# TOUS les hotes du groupe. Mesure du 2026-09-15 sur Chezlepro :
|
||||||
|
#
|
||||||
|
# mon-01 serveur_oauth2_proxy_hostname = console.chezlepro.internal
|
||||||
|
# ops-01 serveur_oauth2_proxy_hostname = console.chezlepro.internal
|
||||||
|
#
|
||||||
|
# La passerelle de la VIGIE se croyait donc la console : son URL de retour OIDC aurait
|
||||||
|
# vise l'autre machine, et le SSO de la supervision serait tombe. Le defaut du
|
||||||
|
# 2026-09-10 — « le service porte le nom que son role devine » — reapparaissait par
|
||||||
|
# une autre porte, et P67 ne le voyait pas puisqu'elle comparait a la valeur qu'elle
|
||||||
|
# derivait elle-meme de la meme facon.
|
||||||
|
#
|
||||||
|
# Une application nomme un COUPLE (machine, role). C'est la clef juste.
|
||||||
|
hostname_par_hote_groupe: dict = {}
|
||||||
for app in apps.values():
|
for app in apps.values():
|
||||||
exp = [str(f).strip() for f in (app.get("expose") or []) if str(f).strip()]
|
exp = [str(f).strip() for f in (app.get("expose") or []) if str(f).strip()]
|
||||||
groupe = str(app.get("groupe") or "")
|
groupe, hote = str(app.get("groupe") or ""), str(app.get("hote") or "")
|
||||||
if groupe and len(exp) == 1:
|
if groupe and hote and len(exp) == 1:
|
||||||
hostname_par_groupe[groupe] = exp[0]
|
hostname_par_hote_groupe[(hote, groupe)] = exp[0]
|
||||||
|
|
||||||
# La patte de la frontiere que les tenants traversent — calculee une fois.
|
# La patte de la frontiere que les tenants traversent — calculee une fois.
|
||||||
_temps_frontiere: list = []
|
_temps_frontiere: list = []
|
||||||
|
|
@ -366,9 +385,10 @@ def generer() -> dict:
|
||||||
hostvars.setdefault("chrony_serveurs", _temps_frontiere)
|
hostvars.setdefault("chrony_serveurs", _temps_frontiere)
|
||||||
|
|
||||||
# Nom public de chaque service porte par cet hote, tel que le plan l'expose.
|
# Nom public de chaque service porte par cet hote, tel que le plan l'expose.
|
||||||
for groupe_expose in sorted(groupes & set(hostname_par_groupe)):
|
for groupe_expose in sorted(groupes):
|
||||||
hostvars.setdefault(f"{groupe_expose}_hostname",
|
fqdn = hostname_par_hote_groupe.get((nom, groupe_expose))
|
||||||
hostname_par_groupe[groupe_expose])
|
if fqdn:
|
||||||
|
hostvars.setdefault(f"{groupe_expose}_hostname", fqdn)
|
||||||
# Le resolveur de l'ecosysteme doit savoir a qui demander la zone de l'hebergeur.
|
# Le resolveur de l'ecosysteme doit savoir a qui demander la zone de l'hebergeur.
|
||||||
if "serveur_resolveur" in groupes and zones_deleguees:
|
if "serveur_resolveur" in groupes and zones_deleguees:
|
||||||
hostvars["serveur_resolveur_zones_deleguees"] = zones_deleguees
|
hostvars["serveur_resolveur_zones_deleguees"] = zones_deleguees
|
||||||
|
|
|
||||||
|
|
@ -3507,14 +3507,15 @@ def preuve_nom_public_vient_du_plan() -> tuple[bool, str]:
|
||||||
verifies = 0
|
verifies = 0
|
||||||
for etiquette, attendu, rendu in lots:
|
for etiquette, attendu, rendu in lots:
|
||||||
enfants, hv = rendu["enfants"], rendu["hv"]
|
enfants, hv = rendu["enfants"], rendu["hv"]
|
||||||
for groupe, fqdn in sorted(attendu.items()):
|
for (hote, groupe), fqdn in sorted(attendu.items()):
|
||||||
for hote in sorted((enfants.get(groupe) or {}).get("hosts") or {}):
|
if hote not in ((enfants.get(groupe) or {}).get("hosts") or {}):
|
||||||
vu = str((hv.get(hote) or {}).get(f"{groupe}_hostname") or "")
|
continue # le plan declare l'app ailleurs que la ou le groupe est pose
|
||||||
verifies += 1
|
vu = str((hv.get(hote) or {}).get(f"{groupe}_hostname") or "")
|
||||||
if vu != fqdn:
|
verifies += 1
|
||||||
fautes.append(f"[{etiquette}] {hote} porte {groupe} sans "
|
if vu != fqdn:
|
||||||
f"`{groupe}_hostname` = `{fqdn}` "
|
fautes.append(f"[{etiquette}] {hote} porte {groupe} sans "
|
||||||
f"(inventaire : {vu or 'absent'})")
|
f"`{groupe}_hostname` = `{fqdn}` "
|
||||||
|
f"(inventaire : {vu or 'absent'})")
|
||||||
if fautes:
|
if fautes:
|
||||||
return False, ("Un service ne porte pas le nom que le plan lui donne — ses URL de "
|
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]))
|
"retour viseront l'ancien :\n - " + "\n - ".join(fautes[:6]))
|
||||||
|
|
@ -3526,16 +3527,24 @@ def preuve_nom_public_vient_du_plan() -> tuple[bool, str]:
|
||||||
|
|
||||||
|
|
||||||
def _noms_exposes(f_app) -> dict:
|
def _noms_exposes(f_app) -> dict:
|
||||||
"""Groupe -> le nom unique que le plan lui donne. Deux noms : aucun canonique."""
|
"""(machine, groupe) -> le nom unique que le plan lui donne.
|
||||||
|
|
||||||
|
PAR HOTE, ET PAS PAR GROUPE SEUL (2026-09-15). Cette preuve indexait par groupe, comme
|
||||||
|
la derivation qu'elle garde — elle validait donc la meme erreur des deux cotes. Sur
|
||||||
|
Chezlepro, `serveur_oauth2_proxy` sert la vigie sur un hote et la console sur un
|
||||||
|
autre : les deux ont recu le DERNIER nom, et la preuve a rendu vert.
|
||||||
|
|
||||||
|
Une garde qui reproduit le raisonnement qu'elle verifie ne verifie rien.
|
||||||
|
"""
|
||||||
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[tuple[str, str], str] = {}
|
||||||
for a in apps.values():
|
for a in apps.values():
|
||||||
if not isinstance(a, dict):
|
if not isinstance(a, dict):
|
||||||
continue
|
continue
|
||||||
exp = [str(f).strip() for f in (a.get("expose") or []) if str(f).strip()]
|
exp = [str(f).strip() for f in (a.get("expose") or []) if str(f).strip()]
|
||||||
g = str(a.get("groupe") or "")
|
g, h = str(a.get("groupe") or ""), str(a.get("hote") or "")
|
||||||
if g and len(exp) == 1:
|
if g and h and len(exp) == 1:
|
||||||
attendu[g] = exp[0]
|
attendu[(h, g)] = exp[0]
|
||||||
return attendu
|
return attendu
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -519,12 +519,16 @@ def inventaire() -> dict:
|
||||||
# UNE SEULE EXPOSITION, sinon rien : deux noms ne designent aucun canonique, et le
|
# 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
|
# role garde alors la main. `setdefault` cote hote : une valeur ecrite a la main dans
|
||||||
# les variables d'une machine gagne encore.
|
# les variables d'une machine gagne encore.
|
||||||
_hostname_par_groupe: dict = {}
|
# PAR HOTE *ET* PAR GROUPE (2026-09-15) — un role peut servir deux applications sur
|
||||||
|
# deux machines, et la table indexee par groupe seul donnait le DERNIER nom a tous.
|
||||||
|
# Voir `instancier.py`, ou le meme defaut a fait croire a la passerelle de la vigie
|
||||||
|
# qu'elle etait la console.
|
||||||
|
_hostname_par_hote_groupe: dict = {}
|
||||||
for _app in applications.values():
|
for _app in applications.values():
|
||||||
_exp = [str(f).strip() for f in (_app.get("expose") or []) if str(f).strip()]
|
_exp = [str(f).strip() for f in (_app.get("expose") or []) if str(f).strip()]
|
||||||
_g = str(_app.get("groupe") or "")
|
_g, _h = str(_app.get("groupe") or ""), str(_app.get("hote") or "")
|
||||||
if _g and len(_exp) == 1:
|
if _g and _h and len(_exp) == 1:
|
||||||
_hostname_par_groupe[_g] = _exp[0]
|
_hostname_par_hote_groupe[(_h, _g)] = _exp[0]
|
||||||
|
|
||||||
hostvars: dict[str, dict] = {}
|
hostvars: dict[str, dict] = {}
|
||||||
groupes: dict[str, list[str]] = {}
|
groupes: dict[str, list[str]] = {}
|
||||||
|
|
@ -759,9 +763,10 @@ def inventaire() -> dict:
|
||||||
**(srv.get("variables") or {}),
|
**(srv.get("variables") or {}),
|
||||||
}
|
}
|
||||||
# Nom public de chaque service porte par cet hote, tel que le plan l'expose.
|
# 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)):
|
for _groupe_expose in sorted(_groupes_de(nom)):
|
||||||
hostvars[nom].setdefault(f"{_groupe_expose}_hostname",
|
_fqdn = _hostname_par_hote_groupe.get((nom, _groupe_expose))
|
||||||
_hostname_par_groupe[_groupe_expose])
|
if _fqdn:
|
||||||
|
hostvars[nom].setdefault(f"{_groupe_expose}_hostname", _fqdn)
|
||||||
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