From 41413c7f04a1bdfe03804a848ba5cd67f6358aa6 Mon Sep 17 00:00:00 2001 From: Daniel Allaire Date: Fri, 7 Aug 2026 09:30:48 -0400 Subject: [PATCH] deploiement : deux attentes de premier demarrage, et trois defauts leves MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `idm-01` a prouve le routage inter-zone dans le VRF (10.27.19.21 -> 10.27.17.11, deux passerelles anycast) et leve trois defauts qu'aucun devis ne pouvait montrer : 1. Le handler de `client_pki` rechargeait `slapd` avant son installation — consequence directe de l'ordre retabli. Un consommateur absent n'est pas une erreur ; un vrai echec de rechargement reste fatal. 2. `resoudre_base` ne trouvait aucune base de portee `application` : Keycloak declare `consommateur: keycloak`, le role cherchait `serveur_keycloak`. Le lien est declare dans applications.yml — on le suit au lieu de retirer un prefixe a la main. 3. Keycloak attend PostgreSQL, pas encore deploye. Pas un defaut : `deployer` ne connait que l'ordre intra-hote, l'ordre inter-hotes est celui de `site`. Et deux attentes, sans lesquelles on ne peut pas enchainer creation et deploiement — donc sans lesquelles `myDay` ne reconstruit pas seul : SSH, puis cloud-init et les maj automatiques. `creer-vm` rend desormais une VM PRETE. `lock_timeout: 300` sur chaque tache apt complete l'attente : le verrou peut etre repris ENTRE deux taches, ce qu'une verification en amont ne previent pas. Defaut dans l'attente elle-meme : `unattended-upgrades` est un demon, toujours actif — l'inclure rendait la condition impossible a satisfaire. Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 51 +++++++++++++++++++++++++ Makefile | 51 ++++++++++++++++++++++++- roles/client_pki/handlers/main.yml | 10 +++++ roles/common_packages/defaults/main.yml | 4 ++ roles/common_packages/tasks/main.yml | 11 ++++++ roles/resoudre_base/tasks/main.yml | 29 ++++++++++++-- 6 files changed, 152 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a7bf917..7b2753c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,57 @@ ## 2026-08-06 — le chemin nord-sud devient dérivable +### Trois défauts que seul un vrai déploiement pouvait montrer + +`idm-01` — première VM d'une autre zone de sécurité (`t17iden`) — a prouvé le **routage +inter-zone** dans le VRF : `10.27.19.21` joint `10.27.17.11`, à travers deux passerelles +anycast. Puis elle a levé trois défauts, tous invisibles jusqu'à ce qu'on déploie pour de vrai. + +**Le handler de `client_pki` rechargeait un service pas encore installé.** Conséquence directe +de l'ordre rétabli : `client_pki` s'exécute **avant** les services — ils ont besoin du +certificat — et son handler tentait `systemctl reload slapd`. Un consommateur absent n'est pas +une erreur : il prendra le certificat déjà posé à son installation. Le silence est resserré sur +ce cas précis ; un vrai échec de rechargement reste fatal, sinon un service servirait un +certificat périmé sans que personne ne l'apprenne. + +**`resoudre_base` ne trouvait aucune base de portée `application`.** Le registre accepte deux +portées : `groupe` désigne un groupe opérationnel, `application` une application du plan. +Keycloak déclare `consommateur: keycloak` — valide, le validateur l'accepte — mais le rôle +cherchait `serveur_keycloak`. Le lien entre les deux était **déclaré** (`applications.yml` +nomme le `groupe` de chaque application) : on le suit désormais, plutôt que de retirer un +préfixe à la main. Une convention de nommage se contredit un jour ; une déclaration se corrige. + +**Keycloak attend une base qui n'existe pas encore.** `data-sql-01` n'est pas déployée : le +service démarre, se connecte, échoue. Ce n'est pas un défaut — `make deployer HOTE=x` ne connaît +que l'ordre **intra-hôte**. L'ordre inter-hôtes existe (`couches-deploiement.yml` place +`serveur_postgresql` avant les applications) et c'est `make site` qui l'exploite. + +### Deux attentes, sans lesquelles `make myDay` ne peut pas reconstruire + +Enchaîner `creer-vm` puis `deployer` échouait presque toujours sur une machine neuve, pour deux +raisons distinctes : + +- **SSH n'est pas levé** quand Proxmox rend la main. Le symptôme trompe : à travers la + frontière le TCP s'établit (SYN proxy) et l'échec se lit *« timed out during banner + exchange »*. +- **Le verrou dpkg** est tenu par les mises à jour automatiques de Debian, par vagues, pendant + plusieurs minutes après le premier démarrage. + +`_attendre-hote` attend les deux, et `creer-vm` rend désormais une VM **prête** plutôt que +seulement démarrée. `ATTENTE_HOTE` (600 s) borne l'attente : dépasser reste un échec, pour +qu'une panne ne devienne pas une attente infinie. + +**Deux couches, pas une.** Le premier essai vérifiait le verrou avec `fuser` — un instantané. +Il était libre au test, repris juste après. Chaque tâche `apt` de `common_packages` porte donc +`lock_timeout: 300` : le Makefile attend la fin des vagues, le rôle survit à une vague qui +repart **entre** deux tâches. + +Et un défaut dans l'attente elle-même : `unattended-upgrades.service` est un **démon** +(`Type=simple`), toujours `active`. L'inclure dans la condition la rendait impossible à +satisfaire — elle échouait au délai, systématiquement. Seules les unités `apt-daily*` sont des +one-shot ; c'est le verrou qui dit si dpkg est occupé. + + ### `make deployer` ignorait l'ordre des couches `client_metrique` échouait sur les deux premières VM : il exige le certificat TLS du diff --git a/Makefile b/Makefile index b5e4a8a..ab7dca5 100644 --- a/Makefile +++ b/Makefile @@ -666,6 +666,12 @@ creer-vm: _instance-requise CLE_SSH_PUBLIQUE="$(CLE_SSH_PUBLIQUE)" \ DEMARRER="$(DEMARRER)" \ CLONE_COMPLET="$(CLONE_COMPLET)" + @# `creer-vm` rend une VM PRETE, pas seulement demarree : sans cette attente, + @# enchainer `creer-vm` puis `deployer` echoue presque toujours sur une machine + @# neuve. C'est ce qui separe une suite de commandes d'une reconstruction. + @if [[ "$(ATTENDRE)" != "false" ]]; then \ + $(MAKE) --no-print-directory _attendre-hote LIMITE="$(HOTE)"; \ + fi inventaire-verifier: ansible-runtime _instance-requise ansible-inventory -i $(INVENTAIRE_LAB) --list > /dev/null @@ -756,7 +762,50 @@ nettoyer-modele: ansible-runtime fi ansible-playbook -i $(INVENTAIRE_LAB) $(PLAYBOOK_NETTOYER_MODELE) -e template_cleanup_confirm=true -.PHONY: _verifier-acces-hote _verifier-privileges-hote faits verifier-hote +.PHONY: _verifier-acces-hote _verifier-privileges-hote _attendre-hote faits verifier-hote + +# Deux ATTENTES ACTIVES, sans lesquelles on ne peut pas enchainer creation et +# deploiement — donc sans lesquelles `make myDay` ne peut pas reconstruire seul. +# +# 1. SSH. `make creer-vm` rend la main des que Proxmox a DEMARRE la VM, pas quand elle +# repond. Verifier l'acces aussitot echoue presque toujours sur une machine neuve — +# et le symptome trompe : a travers la frontiere, le TCP s'etablit (SYN proxy) et +# l'echec se lit « Connection timed out during banner exchange ». +# +# 2. Le verrou dpkg. L'image Debian lance ses propres mises a jour au premier +# demarrage et tient `/var/lib/dpkg/lock-frontend` plusieurs minutes. Le premier +# `apt` d'Ansible echoue alors sur un verrou, pas sur une vraie erreur. +# +# `unattended-upgrades.service` est volontairement ABSENT de la condition : c'est un +# DEMON (Type=simple), toujours `active`. L'y inclure rendait l'attente impossible a +# satisfaire — elle echouait au bout du delai, systematiquement. Seules `apt-daily*` +# sont des one-shot, et c'est le VERROU qui dit si dpkg est reellement occupe. +# +# Les deux sont des COURSES de premier demarrage, pas des defauts de conception. On les +# attend au lieu de les subir. ATTENTE_HOTE (secondes) borne chacune : depasser le +# delai reste un echec, pour ne pas transformer une panne en attente infinie. +ATTENTE_HOTE ?= 600 + +_attendre-hote: ansible-runtime + @set -e; \ + if [[ -z "$(LIMITE)" ]]; then printf '%s\n' 'Refus: LIMITE requis.'; exit 2; fi; \ + fin=$$(( SECONDS + $(ATTENTE_HOTE) )); \ + printf '%s' "Attente de SSH sur $(LIMITE) "; \ + until ansible -i $(INVENTAIRE_PRODUCTION) $(LIMITE) -m ping -e ansible_become=false >/dev/null 2>&1; do \ + if (( SECONDS > fin )); then printf '%s\n' " ECHEC: injoignable apres $(ATTENTE_HOTE)s."; exit 4; fi; \ + printf '.'; sleep 5; \ + done; \ + printf '%s\n' " ok"; \ + printf '%s' "Attente de cloud-init et des maj automatiques "; \ + until ansible -i $(INVENTAIRE_PRODUCTION) $(LIMITE) -b -m shell -a \ + 'cloud-init status --wait >/dev/null 2>&1; \ + ! systemctl is-active --quiet apt-daily.service apt-daily-upgrade.service \ + && ! fuser /var/lib/dpkg/lock-frontend >/dev/null 2>&1' >/dev/null 2>&1; do \ + if (( SECONDS > fin )); then printf '%s\n' " ECHEC: dpkg toujours occupe apres $(ATTENTE_HOTE)s."; exit 4; fi; \ + printf '.'; sleep 10; \ + done; \ + printf '%s\n' " libere" + _verifier-acces-hote: ansible-runtime ansible -i $(INVENTAIRE_PRODUCTION) $(LIMITE) -m ping -e ansible_become=false diff --git a/roles/client_pki/handlers/main.yml b/roles/client_pki/handlers/main.yml index b4dce7a..4f56d3a 100644 --- a/roles/client_pki/handlers/main.yml +++ b/roles/client_pki/handlers/main.yml @@ -6,6 +6,12 @@ # Apres une re-emission du cert (ex: nouveau SAN), recharger les services qui le # servent (nginx, postfix, slapd...) sinon ils continuent de servir l'ancien cert. +# +# Un consommateur PAS ENCORE INSTALLE n'est pas une erreur : `client_pki` s'execute +# AVANT les services (couches de deploiement), donc `slapd` n'existe pas quand l'hote +# recoit son premier certificat. Le service prendra le cert deja pose a son +# installation. On ne tait que ce cas precis — un vrai echec de rechargement doit +# rester fatal, sinon un service continuerait a servir un certificat perime en silence. - name: Recharger les consommateurs du cert ansible.builtin.systemd: name: "{{ item }}" @@ -13,6 +19,10 @@ loop: "{{ client_pki_reload_services }}" loop_control: label: "{{ item }}" + register: client_pki_rechargements + failed_when: + - client_pki_rechargements is failed + - "'Could not find the requested service' not in (client_pki_rechargements.msg | default(''))" when: - not ansible_check_mode - client_pki_reload_services | length > 0 diff --git a/roles/common_packages/defaults/main.yml b/roles/common_packages/defaults/main.yml index d93923e..b72dcf7 100644 --- a/roles/common_packages/defaults/main.yml +++ b/roles/common_packages/defaults/main.yml @@ -46,3 +46,7 @@ common_packages_list: - unattended-upgrades - apt-listchanges - needrestart + +# Attente du verrou dpkg (secondes). Genereux : au premier demarrage, les mises a +# jour automatiques de Debian le tiennent par vagues pendant plusieurs minutes. +common_packages_lock_timeout: 300 diff --git a/roles/common_packages/tasks/main.yml b/roles/common_packages/tasks/main.yml index 145e763..cef43e7 100644 --- a/roles/common_packages/tasks/main.yml +++ b/roles/common_packages/tasks/main.yml @@ -1,18 +1,29 @@ --- +# `lock_timeout` sur CHAQUE tache apt : au premier demarrage, l'image Debian lance ses +# propres mises a jour (apt-daily, unattended-upgrades) et tient le verrou dpkg par +# vagues. Sans attente, la premiere tache echoue sur un verrou — pas sur une vraie +# erreur — et le deploiement d'une VM neuve devient un tirage au sort. +# +# Attendre ici plutot que seulement avant le playbook : le verrou peut etre repris +# ENTRE deux taches, ce qu'une verification ponctuelle en amont ne peut pas empecher. - name: Mettre à jour le cache APT ansible.builtin.apt: update_cache: true cache_valid_time: 3600 + lock_timeout: "{{ common_packages_lock_timeout }}" - name: Appliquer les mises à jour disponibles ansible.builtin.apt: upgrade: full + lock_timeout: "{{ common_packages_lock_timeout }}" - name: Installer les paquets communs ansible.builtin.apt: name: "{{ common_packages_list }}" state: present + lock_timeout: "{{ common_packages_lock_timeout }}" - name: Supprimer les dépendances devenues inutiles ansible.builtin.apt: autoremove: true + lock_timeout: "{{ common_packages_lock_timeout }}" diff --git a/roles/resoudre_base/tasks/main.yml b/roles/resoudre_base/tasks/main.yml index 72b7f59..0069a55 100644 --- a/roles/resoudre_base/tasks/main.yml +++ b/roles/resoudre_base/tasks/main.yml @@ -14,12 +14,33 @@ ansible.builtin.include_vars: file: "{{ setops_plan_dir }}/bases-donnees.yml" -- name: Résoudre l'entrée de base (par consommateur) +# Le `consommateur` d'une base n'est PAS toujours un groupe : le validateur du registre +# (inventory_rules.py) accepte deux portées — `groupe` désigne un groupe opérationnel, +# `application` désigne une APPLICATION de plan/applications.yml. Chercher uniquement le +# nom du groupe faisait donc échouer toute base de portée `application` : Keycloak +# déclare `consommateur: keycloak`, le rôle demandait `serveur_keycloak`. +# +# Le lien entre les deux est DÉCLARÉ — chaque application nomme son `groupe`. On le suit +# plutôt que de retirer un préfixe à la main : une convention de nommage se contredit un +# jour, une déclaration se corrige. +- name: Charger le registre des applications (lien application → groupe) + ansible.builtin.include_vars: + file: "{{ setops_plan_dir }}/applications.yml" + +- name: Établir les noms sous lesquels ce groupe peut être consommateur + ansible.builtin.set_fact: + resoudre_base_noms: >- + {{ [resoudre_base_groupe] + ((applications | default({})) | dict2items + | selectattr('value.groupe', 'defined') + | selectattr('value.groupe', 'equalto', resoudre_base_groupe) + | map(attribute='key') | list) }} + +- name: Résoudre l'entrée de base (par consommateur, groupe ou application) ansible.builtin.set_fact: resoudre_base_entree: >- {{ ((bases_donnees | default({})) | dict2items | selectattr('value.consommateur', 'defined') - | selectattr('value.consommateur', 'equalto', resoudre_base_groupe) + | selectattr('value.consommateur', 'in', resoudre_base_noms) | map(attribute='value') | list | first) | default({}) }} - name: Exiger une entrée de registre et son secret @@ -28,7 +49,9 @@ - resoudre_base_entree.base is defined - resoudre_base_entree.proprietaire is defined - resoudre_base_entree.secret is defined - fail_msg: "Aucune base avec consommateur '{{ resoudre_base_groupe }}' dans instance/plan/bases-donnees.yml." + fail_msg: >- + Aucune base dont le `consommateur` soit l'un de {{ resoudre_base_noms }} + dans instance/plan/bases-donnees.yml (portée `groupe` ou `application`). - name: Résoudre le mot de passe de base (Vault) ansible.builtin.set_fact: