diff --git a/CHANGELOG.md b/CHANGELOG.md index cb10bc3..3ad7779 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,59 @@ # CHANGELOG — Set-OPS +## 2026-08-10 — Le jeton Keycloak vivait 60 s, et Grafana rendait son échec illisible + +Technolibre remonté depuis zéro une seconde fois — `14 hôtes · 2 588 tâches ok · 0 failed`. +Deux défauts trouvés en chemin, dont un dont la cause était **restée non établie** le matin +même. + +### Le jeton d'administration Keycloak était pris une fois pour toutes + +Il était obtenu dans `politique-mdp.yml` — le **2ᵉ** des neuf fichiers du rôle — et réutilisé +jusqu'au **8ᵉ**. Or le jeton `admin-cli` du realm `master` vit **60 secondes**. Entre les +deux : six fichiers de travail, dont `groupes-ldap.yml` et ses reprises espacées de 15 s. + +Sur une construction **neuve**, le temps écoulé dépasse la minute et l'appel suivant se prend +un 401. Sur un **rejeu**, tout est convergé, ça va vite, ça passe. D'où deux échecs le matin +même, suivis chaque fois d'un succès au rejeu — ce qui donnait l'illusion d'une course au +démarrage de Keycloak. J'avais écrit alors ne pas avoir de mesure qui le prouve ; c'était la +bonne prudence, et la cause était l'**âge du jeton**. + +`jeton-admin.yml` prend désormais un jeton frais **là où on s'en sert**. C'est gratuit : +Keycloak répond en quelques millisecondes en local. + +### Grafana : un échec transitoire, rendu illisible par systemd + +Au premier démarrage, après **67 secondes** de migrations, `grafana-server` a échoué sur +`failed to create admin user: SQL logic error: no such column: uid` — alors que la migration +qui ajoute cette colonne était journalisée comme **réussie**. Base neuve : tout remigre +correctement, service actif, colonne présente. **L'incident ne s'est pas reproduit, et +Chezlepro ne l'a jamais eu.** + +Je n'ai donc pas corrigé la cause — je ne l'ai pas reproduite. J'ai corrigé ce qui la rendait +indéchiffrable : + +- `Restart=on-failure` venait du paquet **sans `RestartSec`**, donc 100 ms : systemd a relancé + **six fois en une seconde**, chaque relance rejouant les migrations sur la même base SQLite. + Un échec unique se présentait comme un désastre, et il a fallu remonter tout le journal pour + retrouver la **première** erreur — la seule qui disait quelque chose. `RestartSec=10` ; +- la rotation du compte de secours échouait **cinq fois sous `no_log`** en annonçant « the + output has been hidden », alors que la vraie cause était ailleurs et parfaitement lisible : + le serveur ne démarrait pas. Une attente explicite sur le port précède maintenant la CLI, + avec un message qui dit d'aller chercher la **première** erreur du journal. + +**Troisième fois dans la journée que `no_log` masque la cause au moment où elle sert.** Le +motif est constant, et il mérite d'être retenu : *une garde qui protège un secret ne doit pas +emporter le diagnostic avec lui.* + +### Sept devis sur Technolibre + +MTU, identité, certificats, PostgreSQL, courriel et frontière : **CONFORME**. Les expositions +répondent depuis l'edge ; seul le plancher `/etc/hosts` du poste manquait. + +**Le devis du MTU mérite une mention** : c'est la première flotte du dépôt à naître au bon +MTU sans une seule intervention — le gabarit porte `mtu=1`, les quatorze invités sont à 1450 +dès leur premier démarrage. + ## 2026-08-10 — Le MTU de la zone n'atteignait pas les invités Question de l'exploitant : « je ne vois nulle part un MTU à 1450 ». Elle était fondée. diff --git a/roles/serveur_grafana/tasks/main.yml b/roles/serveur_grafana/tasks/main.yml index 9faf7c5..bf561e4 100644 --- a/roles/serveur_grafana/tasks/main.yml +++ b/roles/serveur_grafana/tasks/main.yml @@ -127,6 +127,32 @@ # Idempotence par EMPREINTE : on garde le sha256 du secret applique. Tant qu'il ne # change pas, on ne touche a rien ; des qu'il change, on reapplique. On ne peut pas # lire le mot de passe en place, donc on memorise ce qu'on a pose. +# EXIGER QUE LE SERVEUR REPONDE AVANT DE TOUCHER A SA BASE. La CLI qui suit echouait +# sinon cinq fois de suite sous `no_log`, en annoncant « the output has been hidden » — +# alors que la vraie cause etait ailleurs et parfaitement lisible : `grafana-server` ne +# demarrait pas, sa base ayant echoue une migration au premier lancement (2026-08-10, +# `no such column: uid`). Cinq reprises censurees contre un message clair : on prefere +# le message. +- name: Attendre que Grafana réponde avant de toucher au compte de secours + ansible.builtin.wait_for: + host: 127.0.0.1 + port: "{{ serveur_grafana_port | default(3000) }}" + timeout: 120 + register: serveur_grafana_pret + failed_when: false + when: not ansible_check_mode + +- name: Dire que Grafana ne répond pas, plutôt que d'échouer sur sa CLI + ansible.builtin.fail: + msg: >- + Grafana n'écoute pas sur le port {{ serveur_grafana_port | default(3000) }} après + 120 s : la rotation du compte de secours ne peut pas aboutir, et sa CLI ne dirait + rien d'utile. Regarder `journalctl -u grafana-server` — et y chercher la PREMIÈRE + erreur, pas la dernière : `Restart=on-failure` en empile plusieurs. + when: + - not ansible_check_mode + - serveur_grafana_pret is failed + - name: Empreinte du mot de passe admin en place ansible.builtin.slurp: src: "{{ serveur_grafana_marqueur_admin }}" diff --git a/roles/serveur_grafana/templates/setops.conf.j2 b/roles/serveur_grafana/templates/setops.conf.j2 index 2329eba..0c36853 100644 --- a/roles/serveur_grafana/templates/setops.conf.j2 +++ b/roles/serveur_grafana/templates/setops.conf.j2 @@ -1,4 +1,12 @@ [Service] +# `Restart=on-failure` vient du paquet, SANS `RestartSec` — donc 100 ms par defaut. +# Le 2026-08-10, une migration de base a echoue au premier demarrage : systemd a relance +# six fois en une seconde, chaque relance rejouant les migrations sur la meme base SQLite. +# Un echec unique s'est ainsi presente comme un desastre, et il a fallu remonter tout le +# journal pour retrouver la PREMIERE erreur — la seule qui disait quelque chose. +# +# Dix secondes laissent le temps de voir, de journaliser, et n'empilent pas les migrations. +RestartSec=10 Environment=GF_SERVER_DOMAIN={{ serveur_grafana_hostname }} Environment=GF_SERVER_ROOT_URL={{ serveur_grafana_root_url }} Environment=GF_SECURITY_ADMIN_PASSWORD={{ serveur_grafana_admin_password }} diff --git a/roles/serveur_keycloak/tasks/deconnexion-oidc.yml b/roles/serveur_keycloak/tasks/deconnexion-oidc.yml index c5a2b7d..2527c28 100644 --- a/roles/serveur_keycloak/tasks/deconnexion-oidc.yml +++ b/roles/serveur_keycloak/tasks/deconnexion-oidc.yml @@ -15,13 +15,34 @@ # la commande, sort en succes et n'ecrit rien (mesure du meme jour sur `smtpServer`). # On relit systematiquement apres avoir pose. +# Jeton FRAIS : celui de `politique-mdp.yml` a plus de six fichiers d'age, et il vit +# 60 s. C'est ce qui faisait echouer cette lecture sur toute construction NEUVE — et +# passer au rejeu, une fois la flotte convergee. Voir `jeton-admin.yml`. +- name: Reprendre un jeton d'administration avant de lire les clients + ansible.builtin.include_tasks: jeton-admin.yml + - name: Lire les clients du realm ansible.builtin.uri: url: "http://localhost:8080/admin/realms/{{ serveur_keycloak_realm }}/clients" headers: Authorization: "Bearer {{ serveur_keycloak_pol_jeton.json.access_token }}" + status_code: [200] register: serveur_keycloak_clients_actuels + # `no_log` protege le jeton porte par l'en-tete. Il masquait aussi la CAUSE : le + # 2026-08-10, cet appel a echoue deux fois sur « the output has been hidden », et il a + # fallu remonter jusqu'a l'age du jeton pour comprendre. On extrait donc le verdict. no_log: true + failed_when: false + +- name: Dire pourquoi la lecture des clients a échoué, si elle a échoué + ansible.builtin.fail: + msg: >- + Keycloak a refuse la lecture des clients du realme + « {{ serveur_keycloak_realm }} » (HTTP + {{ serveur_keycloak_clients_actuels.status | default('?') }}). + Un 401 signifie un jeton perime — verifier que `jeton-admin.yml` est bien inclus + juste avant cet appel. + when: serveur_keycloak_clients_actuels.status | default(0) != 200 - name: Composer les URI de déconnexion attendues ansible.builtin.set_fact: diff --git a/roles/serveur_keycloak/tasks/jeton-admin.yml b/roles/serveur_keycloak/tasks/jeton-admin.yml new file mode 100644 index 0000000..adbbfa4 --- /dev/null +++ b/roles/serveur_keycloak/tasks/jeton-admin.yml @@ -0,0 +1,32 @@ +--- +# Un jeton d'administration FRAIS, a inclure juste avant de s'en servir. +# +# POURQUOI CE FICHIER EXISTE. Le jeton etait obtenu une seule fois, dans +# `politique-mdp.yml` — le 2e des neuf fichiers du role — puis reutilise jusqu'au 8e. +# Or le jeton `admin-cli` du realm `master` vit **60 secondes** par defaut. Entre les +# deux, six fichiers de travail, dont `groupes-ldap.yml` et ses reprises espacees de 15 s. +# +# Sur un deploiement NEUF, le temps ecoule depasse la minute et l'appel suivant se prend +# un 401. Sur un REJEU, tout est deja converge, ca va vite, et ca passe. C'est exactement +# ce qu'on a observe le 2026-08-10 : echec sur la construction, succes au rejeu — deux +# fois, ce qui donnait l'illusion d'une course aleatoire au demarrage de Keycloak. +# +# Un jeton se prend donc la ou on l'utilise, jamais « une fois pour toutes ». C'est +# gratuit : Keycloak repond en quelques millisecondes en local. +- name: Obtenir un jeton d'administration frais + ansible.builtin.uri: + url: "http://localhost:8080/realms/master/protocol/openid-connect/token" + method: POST + body_format: form-urlencoded + body: + grant_type: password + client_id: admin-cli + username: "{{ serveur_keycloak_admin_user }}" + password: "{{ serveur_keycloak_admin_password }}" + status_code: [200] + register: serveur_keycloak_pol_jeton + no_log: true + # Keycloak peut encore etre en train de recharger apres une tache precedente. + retries: 5 + delay: 4 + until: serveur_keycloak_pol_jeton is succeeded