--- # CLOUD-INIT REECRIT /etc/hosts A CHAQUE DEMARRAGE (2026-08-24). # # Le gabarit dore laisse `manage_etc_hosts: true`. A chaque boot, cloud-init regenere # /etc/hosts depuis son propre modele — et efface le plancher de resolution que ce role # vient de poser. La machine perd alors la seule facon qu'elle a de nommer ses voisines # sans DNS, et personne ne le voit : rien n'echoue tant qu'on ne demande pas un nom. # # CONSTATE SUR infra-dns-01 : redemarre a 18h53 lors d'un deplacement de disque, il a # perdu ses six entrees de flotte. Le defaut est apparu deux jours plus tard, sous la # forme d'un `apt update` qui n'arrivait plus a resoudre le cache d'artefacts — un # message qui ne parlait pas du tout du vrai probleme. # # On le desactive par un fragment plutot qu'en editant cloud.cfg : le gabarit reste # intact, et `apt purge cloud-init` ou une remise a zero rend la machine a son etat # d'origine. # # `zz-` ET NON `99-` (2026-08-25). Cloud-init lit `cloud.cfg.d` dans l'ordre LEXICAL, et # le DERNIER a parler gagne. Notre fragment s'appelait `99-setops-hosts.cfg` ; le gabarit # dore en porte un autre, `99_chezlepro.cfg`, qui pose `manage_etc_hosts: true`. Or `-` # vaut 0x2D et `_` vaut 0x5F : le fragment du gabarit etait lu APRES le notre, et gagnait. # # Mesure du 2026-08-25 : les cinq machines du site redemarrees avaient perdu tout leur # plancher malgre notre fragment — il etait bien la, simplement sans effet. Un fichier # present et ignore est le pire des deux mondes : il rassure sans rien garantir. # # `zz-` passe apres tout prefixe numerique ET apres `_`. On ne cherche donc plus a # connaitre les fragments des autres : on parle en dernier, quels qu'ils soient. La garde # plus bas verifie que c'est encore vrai sur la machine. - name: Empêcher cloud-init de réécrire le plancher à chaque démarrage ansible.builtin.copy: dest: /etc/cloud/cloud.cfg.d/zz-setops-hosts.cfg content: | # GÉNÉRÉ par Set-OPS (rôle hosts_statiques). NE PAS éditer à la main. # /etc/hosts est le PLANCHER DE RÉSOLUTION de l'écosystème, dérivé de l'inventaire. # Laisser cloud-init le régénérer au démarrage l'effacerait en silence. manage_etc_hosts: false owner: root group: root mode: "0644" when: hosts_statiques_actif | bool # CE FRAGMENT EST DESORMAIS LE SEUL A DECIDER (2026-08-25). # # `cloud_init` en ecrivait un second (`99_setops.cfg`) qui posait `manage_etc_hosts: true`. # Dans `cloud.cfg.d` l'ordre est LEXICAL : `-` (0x2D) passe avant `_` (0x5F), donc le # fragment de `cloud_init` etait lu EN DERNIER et gagnait. Le plancher etait efface a # chaque demarrage, et ca ne se voyait qu'au redemarrage suivant — sous la forme d'un # `apt update` qui ne resolvait plus, ou d'un nom de service qui ne pointait nulle part. # # LE RETRAIT DOIT ETRE AUSSI EXPLICITE QUE LA POSE. Sans la tache ci-dessous, desactiver # `hosts_statiques` laissait le fragment en place : la machine gardait un /etc/hosts fige # que plus personne ne tenait a jour, et cloud-init n'avait plus le droit de le reprendre. # Un etat que ni l'un ni l'autre n'assume. - name: Rendre /etc/hosts à cloud-init quand le plancher est désactivé ansible.builtin.file: path: /etc/cloud/cloud.cfg.d/zz-setops-hosts.cfg state: absent when: not (hosts_statiques_actif | bool) # L'ancien nom, qui ne parlait pas en dernier. Le laisser en place n'aurait rien casse, # mais deux fichiers disant la meme chose sont un desaccord qui attend son heure. - name: Retirer l'ancien fragment, qui ne parlait pas en dernier ansible.builtin.file: path: /etc/cloud/cloud.cfg.d/99-setops-hosts.cfg state: absent # Alias d'exposition (best-effort) : dérivés du plan si celui-ci est disponible. - name: Vérifier la présence des registres du plan (sur le nœud de contrôle) ansible.builtin.stat: path: "{{ item }}" register: hosts_statiques_plan delegate_to: localhost # les registres du plan vivent sur le nœud de contrôle become: false # lire un fichier ne nécessite pas de privilèges loop: - "{{ setops_plan_dir }}/applications.yml" - "{{ setops_plan_dir }}/domaines.yml" loop_control: label: "{{ item | basename }}" when: - hosts_statiques_actif | bool - hosts_statiques_publier_expositions | bool - setops_plan_dir is defined # CHAQUE REGISTRE EST CHARGÉ POUR LUI-MÊME (2026-08-25). # # La condition exigeait que les DEUX fichiers existent (`length == 2`). Un tout-ou-rien # sans raison : `domaines.yml` déclare les domaines PUBLICS, et un écosystème peut # légitimement n'en avoir aucun — c'est le cas du SITE, dont tous les services sont # internes. # # Conséquence : `applications.yml` n'était pas chargé non plus, la dérivation rendait une # liste vide, et le plancher s'écrivait SANS AUCUN ALIAS — sans erreur. Le certificat de # la forge portait `forge.genese.internal` et ce nom ne résolvait nulle part. # # On charge donc ce qui existe, fichier par fichier. Un registre absent n'est pas une # faute ; il est simplement absent. - name: Charger les registres applications et domaines (si présents) ansible.builtin.include_vars: file: "{{ item.item }}" loop: "{{ hosts_statiques_plan.results | default([]) }}" loop_control: label: "{{ item.item | basename }}" when: - hosts_statiques_actif | bool - hosts_statiques_publier_expositions | bool - item.stat.exists | default(false) - name: Dériver les alias d'exposition (FQDN exposés → edge qui les sert) ansible.builtin.set_fact: hosts_statiques_expositions: >- {{ {'applications': applications | default({})} | expositions_des_applications({'domaines_publics': domaines_publics | default({})}) }} when: - hosts_statiques_actif | bool - hosts_statiques_publier_expositions | bool - name: Générer /etc/hosts depuis l'inventaire (plancher de résolution, indépendant du DNS) ansible.builtin.template: src: hosts.j2 dest: /etc/hosts owner: root group: root mode: "0644" when: hosts_statiques_actif | bool # LE SEUL ENDROIT QUI TIENNE AU REDEMARRAGE (2026-08-25). # # Le fragment `cloud.cfg.d` ne suffit pas, et c'est mesure : Proxmox inscrit # `manage_etc_hosts: true` dans la USER-DATA de son lecteur cloud-init, et la user-data # prime sur tout fragment de `/etc/cloud/cloud.cfg.d`. Le plancher etait donc efface a # chaque demarrage malgre le fragment — d'abord `99-setops-hosts.cfg`, puis son # remplacant `zz-` : renommer pour parler en dernier ne changeait rien, puisque le dernier # mot n'appartenait pas a ce repertoire. # # Ce que cloud-init regenere, il le regenere DEPUIS CE GABARIT. On le pose donc avec le # meme contenu que /etc/hosts : le fichier de la machine et le gabarit qui le reecrira # disent la meme chose, quelle que soit la valeur de `manage_etc_hosts`. # # Le gabarit d'origine le documente lui-meme : « make changes to the master file in # /etc/cloud/templates/hosts.debian.tmpl ». - name: Poser le plancher dans le gabarit maître de cloud-init (il survit au démarrage) ansible.builtin.template: src: hosts.debian.tmpl.j2 dest: /etc/cloud/templates/hosts.debian.tmpl owner: root group: root mode: "0644" when: - hosts_statiques_actif | bool - ansible_facts.os_family | default('') == 'Debian' # LA GARDE MESURE LE FAIT QUI COMPTE (2026-08-25). # # La garde precedente verifiait que notre fragment `cloud.cfg.d` parlait en dernier. C'est # vrai — et sans effet, puisque la user-data de la source de donnees prime sur ce # repertoire tout entier. Une garde qui mesure la mauvaise chose est pire qu'aucune : elle # affirme « conforme » pendant que le defaut opere. # # Ce qui compte est que le fichier ET le gabarit qui le reecrira portent les MEMES # entrees. On les compare donc, ligne d'adresse par ligne d'adresse. - name: Relever les entrées du plancher et celles du gabarit maître ansible.builtin.shell: cmd: >- set -o pipefail; for f in /etc/hosts /etc/cloud/templates/hosts.debian.tmpl; do printf '%s=' "$f"; grep -cE '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+[[:space:]]' "$f" 2>/dev/null || printf '0'; printf '\n'; done executable: /bin/bash register: hosts_statiques_accord changed_when: false when: - hosts_statiques_actif | bool - ansible_facts.os_family | default('') == 'Debian' - name: Refuser que le plancher et son gabarit maître divergent ansible.builtin.assert: that: - (hosts_statiques_accord.stdout_lines | select('search', '^/etc/hosts=') | first | default('=0')).split('=')[1] | int > 1 - (hosts_statiques_accord.stdout_lines | select('search', '^/etc/hosts=') | first | default('=0')).split('=')[1] == (hosts_statiques_accord.stdout_lines | select('search', 'hosts.debian.tmpl=') | first | default('=-1')).split('=')[1] fail_msg: >- Le plancher et le gabarit maître de cloud-init ne portent pas le même nombre d'entrées ({{ hosts_statiques_accord.stdout_lines | default([]) | join(' | ') }}). Cloud-init régénère /etc/hosts depuis le gabarit à chaque démarrage : la divergence ne se verrait qu'au prochain redémarrage, et se manifesterait ailleurs — un `apt` qui ne résout plus, un certificat dont le nom ne pointe nulle part. success_msg: >- Plancher et gabarit maître d'accord ({{ hosts_statiques_accord.stdout_lines | default([]) | join(' | ') }}). when: - hosts_statiques_actif | bool - ansible_facts.os_family | default('') == 'Debian'