site : le plancher survit au redemarrage, et la zone dit les vraies adresses
Some checks are pending
verifier / verifier (push) Waiting to run
Some checks are pending
verifier / verifier (push) Waiting to run
Le decoupage du site en quatre zones a deplace cinq machines. Ni le plancher
/etc/hosts ni la zone DNS n'avaient suivi. Quatre defauts, tous dans le moteur.
LE PLANCHER ETAIT EFFACE A CHAQUE DEMARRAGE — ET LE PREMIER CORRECTIF N'EN
ETAIT PAS UN.
`hosts_statiques` posait `99-setops-hosts.cfg` avec `manage_etc_hosts: false`,
pendant que `cloud_init` posait `99_setops.cfg` avec `true`. Dans `cloud.cfg.d`
l'ordre est LEXICAL et le dernier gagne : `-` vaut 0x2D, `_` vaut 0x5F. On a
donc retire la cle de `cloud_init` — le role qui POSSEDE le fichier decide —
puis renomme notre fragment `zz-` pour passer apres le `99_chezlepro.cfg` du
gabarit dore.
Et ca ne suffisait toujours pas. Redemarrage d'epreuve : plancher encore efface.
La cause reelle est ailleurs — Proxmox inscrit `manage_etc_hosts: true` dans la
USER-DATA de son lecteur cloud-init, et la user-data prime sur `cloud.cfg.d`
tout entier. Aucun fragment ne pouvait gagner ; renommer pour parler en dernier
ne servait a rien, le dernier mot n'appartenant pas a ce repertoire.
Ce que cloud-init regenere, il le regenere depuis `hosts.debian.tmpl` — le
gabarit le documente lui-meme. `hosts_statiques` le pose desormais avec le MEME
contenu que /etc/hosts, et une garde compare les deux a chaque passage.
Redemarrage d'epreuve : les neuf entrees sont la.
La garde precedente affirmait « conforme » en mesurant l'ordre lexical — vrai,
et sans rapport avec ce qui se passait. Une garde qui mesure la mauvaise chose
est pire qu'aucune.
LA ZONE DNS NE PUBLIAIT PAS LES NOMS DE SERVICE.
`forge.genese.internal` et `pki.genese.internal` — des noms que les certificats
portent et que les clients appellent — n'avaient aucun enregistrement. Seul le
plancher savait les resoudre. Trois causes empilees :
- le plan du site coupait `serveur_powerdns_publier_expositions`, au motif que
« le site n'a pas d'edge » : ca confondait PUBLIC et EXPOSE ;
- `expositions_des_applications` rendait `domaine: None` faute de
`domaines.yml`, et le modele de zone ecarte les expositions dont le domaine
n'est pas la zone. Repli ajoute, symetrique de celui deja ecrit pour `edge` :
sans domaine public declare, le domaine est celui que porte le FQDN ;
- `serveur_powerdns` exigeait les deux registres et echouait si `domaines.yml`
manquait — le meme tout-ou-rien que `hosts_statiques` a corrige le meme jour.
Puis `named-checkzone` a refuse la zone : `dns.genese.internal` heritait d'un
CNAME par defaut du role ET d'un A par exposition. La garde a bien joue son role
— elle a arrete une zone cassee avant qu'elle soit servie. Le plan l'emporte
desormais sur le defaut du role.
Un service ne doit pas dependre d'un plancher pour etre joignable : le plancher
est un filet, pas le sol.
VERIFICATION. Les huit noms — cinq machines, trois services — resolvent vers les
bonnes adresses depuis les cinq hotes, par le plancher ET par le DNS, et le
plancher survit au redemarrage.
P46 refuse desormais deux choses : plus d'un role ecrivant `manage_etc_hosts`,
et l'absence du gabarit maitre. Controle negatif verifie.
RESTE NOMME, PAS CORRIGE : la zone INVERSE. `serveur_powerdns_zone_inverse`
derive d'un supernet /16 — la forme d'un tenant. Un site declare plusieurs /24
et n'a pas de supernet unique : aucune zone inverse n'est generee.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
ac48b7650b
commit
a74e35bf21
10 changed files with 333 additions and 8 deletions
63
CHANGELOG.md
63
CHANGELOG.md
|
|
@ -1,5 +1,68 @@
|
||||||
# CHANGELOG — Set-OPS
|
# CHANGELOG — Set-OPS
|
||||||
|
|
||||||
|
## 2026-08-25 — Le plancher survit au redémarrage, et la zone dit les vraies adresses
|
||||||
|
|
||||||
|
**46 preuves.** Le découpage du site en quatre zones a déplacé cinq machines ; ni le
|
||||||
|
plancher `/etc/hosts` ni la zone DNS n'avaient suivi. Quatre défauts, tous dans le moteur.
|
||||||
|
|
||||||
|
### Le plancher était effacé à chaque démarrage — et le correctif d'hier n'en était pas un
|
||||||
|
|
||||||
|
`hosts_statiques` posait `/etc/cloud/cloud.cfg.d/99-setops-hosts.cfg` avec
|
||||||
|
`manage_etc_hosts: false`, pendant que `cloud_init` posait `99_setops.cfg` avec `true`.
|
||||||
|
Dans `cloud.cfg.d` l'ordre est **lexical** et le dernier gagne : `-` vaut 0x2D, `_` vaut
|
||||||
|
0x5F — le `true` l'emportait. On a donc d'abord retiré la clé de `cloud_init` (le rôle qui
|
||||||
|
**possède** le fichier décide), puis renommé notre fragment `zz-` pour passer après le
|
||||||
|
`99_chezlepro.cfg` du gabarit doré.
|
||||||
|
|
||||||
|
**Et ça ne suffisait toujours pas.** Redémarrage d'épreuve : le plancher, encore effacé.
|
||||||
|
La cause réelle est ailleurs — Proxmox inscrit `manage_etc_hosts: true` dans la
|
||||||
|
**user-data** de son lecteur cloud-init, et la user-data prime sur `cloud.cfg.d` tout
|
||||||
|
entier. Aucun fragment ne pouvait gagner ; renommer pour parler en dernier ne servait à
|
||||||
|
rien, le dernier mot n'appartenant pas à ce répertoire.
|
||||||
|
|
||||||
|
Ce que cloud-init régénère, il le régénère depuis `hosts.debian.tmpl` — le gabarit le
|
||||||
|
documente lui-même. `hosts_statiques` le pose donc désormais avec le **même contenu** que
|
||||||
|
`/etc/hosts`, et une garde compare les deux à chaque passage. Redémarrage d'épreuve : les
|
||||||
|
neuf entrées sont là.
|
||||||
|
|
||||||
|
*La garde précédente affirmait « conforme » en mesurant l'ordre lexical — vrai, et sans
|
||||||
|
rapport avec ce qui se passait. Une garde qui mesure la mauvaise chose est pire
|
||||||
|
qu'aucune.*
|
||||||
|
|
||||||
|
### La zone DNS ne publiait pas les noms de service
|
||||||
|
|
||||||
|
`forge.genese.internal` et `pki.genese.internal` — des noms que les certificats portent et
|
||||||
|
que les clients appellent — n'avaient **aucun enregistrement**. Seul le plancher savait
|
||||||
|
les résoudre. Trois causes empilées :
|
||||||
|
|
||||||
|
- le plan du site coupait `serveur_powerdns_publier_expositions`, au motif que « le site
|
||||||
|
n'a pas d'edge » — ce qui confondait *public* et *exposé* ;
|
||||||
|
- `expositions_des_applications` rendait `domaine: None` faute de `domaines.yml`, et le
|
||||||
|
modèle de zone écarte les expositions dont le domaine n'est pas la zone. Le repli existe
|
||||||
|
désormais : **sans domaine public déclaré, le domaine est celui que porte le FQDN** —
|
||||||
|
symétrique du repli déjà écrit pour `edge` ;
|
||||||
|
- `serveur_powerdns` exigeait les deux registres et échouait si `domaines.yml` manquait —
|
||||||
|
le même tout-ou-rien que `hosts_statiques` avait corrigé le même jour.
|
||||||
|
|
||||||
|
Puis `named-checkzone` a refusé la zone : `dns.genese.internal` héritait d'un `CNAME` par
|
||||||
|
défaut du rôle **et** d'un `A` par exposition. La garde a bien joué son rôle — elle a
|
||||||
|
arrêté une zone cassée avant qu'elle soit servie. Le plan l'emporte désormais sur le
|
||||||
|
défaut du rôle.
|
||||||
|
|
||||||
|
*Un service ne doit pas dépendre d'un plancher pour être joignable : le plancher est un
|
||||||
|
filet, pas le sol.*
|
||||||
|
|
||||||
|
### Vérification
|
||||||
|
|
||||||
|
Les huit noms — cinq machines et trois services — résolvent vers les bonnes adresses
|
||||||
|
depuis les cinq hôtes, **par le plancher et par le DNS**, et le plancher survit au
|
||||||
|
redémarrage.
|
||||||
|
|
||||||
|
**Reste nommé, pas corrigé** : la zone INVERSE. `serveur_powerdns_zone_inverse` dérive
|
||||||
|
d'un supernet `/16` — la forme d'un tenant. Un site déclare plusieurs `/24` et n'a pas de
|
||||||
|
supernet unique : aucune zone inverse n'est générée, et les machines du site restent
|
||||||
|
anonymes à l'envers.
|
||||||
|
|
||||||
## 2026-08-25 — Le pare-feu Proxmox ne s'arme que dans le SDN
|
## 2026-08-25 — Le pare-feu Proxmox ne s'arme que dans le SDN
|
||||||
|
|
||||||
**45 preuves.** Le découpage du site en quatre zones a révélé un défaut qui dormait dans
|
**45 preuves.** Le découpage du site en quatre zones a révélé un défaut qui dormait dans
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
> [`docs/audit/affirmations.md`](affirmations.md).
|
> [`docs/audit/affirmations.md`](affirmations.md).
|
||||||
|
|
||||||
- **Instance** : `/home/danallaire/Espace Chezlepro/DépôtsSurForge/Set-OPS-public/instance` — inventaire `/home/danallaire/Espace Chezlepro/DépôtsSurForge/Set-OPS-public/instance/inventories/production/hosts.yml`
|
- **Instance** : `/home/danallaire/Espace Chezlepro/DépôtsSurForge/Set-OPS-public/instance` — inventaire `/home/danallaire/Espace Chezlepro/DépôtsSurForge/Set-OPS-public/instance/inventories/production/hosts.yml`
|
||||||
- **Verdict** : ✅ CONFORME (44 OK · 0 echec · 1 saute)
|
- **Verdict** : ✅ CONFORME (45 OK · 0 echec · 1 saute)
|
||||||
|
|
||||||
## Preuves
|
## Preuves
|
||||||
|
|
||||||
|
|
@ -58,6 +58,7 @@
|
||||||
| P43 | Frontiere : le devis voit les machines du site | — | ✅ OK | Devis de la frontiere : 5 machine(s) du plan retrouvees, 56 regle(s) du site. |
|
| P43 | Frontiere : le devis voit les machines du site | — | ✅ OK | Devis de la frontiere : 5 machine(s) du plan retrouvees, 56 regle(s) du site. |
|
||||||
| P44 | Integrations : le serveur avant ses clients | — | ✅ OK | 4 integration(s) appliquent leur serveur avant leurs clients. |
|
| P44 | Integrations : le serveur avant ses clients | — | ✅ OK | 4 integration(s) appliquent leur serveur avant leurs clients. |
|
||||||
| P45 | Pare-feu Proxmox : arme sur les VNet SDN, jamais ailleurs | — | ✅ OK | Le pare-feu Proxmox ne s'arme que sur un VNet SDN (4 cas evalues, dont un qui doit rendre VRAI). |
|
| P45 | Pare-feu Proxmox : arme sur les VNet SDN, jamais ailleurs | — | ✅ OK | Le pare-feu Proxmox ne s'arme que sur un VNet SDN (4 cas evalues, dont un qui doit rendre VRAI). |
|
||||||
|
| P46 | Plancher /etc/hosts : un seul role en decide | — | ✅ OK | Un seul maitre du plancher — roles/hosts_statiques/tasks/main.yml : manage_etc_hosts: false ; et le gabarit maitre est pose (roles/hosts_statiques/templates/hos |
|
||||||
|
|
||||||
## Couverture des affirmations ✅ du registre
|
## Couverture des affirmations ✅ du registre
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
---
|
---
|
||||||
cloud_init_ssh_pwauth: false
|
cloud_init_ssh_pwauth: false
|
||||||
cloud_init_manage_etc_hosts: true
|
# `cloud_init_manage_etc_hosts` A ETE RETIREE (2026-08-25) : elle entrait en conflit avec
|
||||||
|
# le fragment de `hosts_statiques`, qui possede /etc/hosts et decide seul si cloud-init
|
||||||
|
# peut y toucher. La garder ici, sans effet, serait un mensonge — une variable qu'on
|
||||||
|
# renseigne et qui ne change rien est pire que son absence.
|
||||||
cloud_init_disable_root: true
|
cloud_init_disable_root: true
|
||||||
cloud_init_ssh_deletekeys: true
|
cloud_init_ssh_deletekeys: true
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,23 @@
|
||||||
datasource_list: [ NoCloud, ConfigDrive, None ]
|
datasource_list: [ NoCloud, ConfigDrive, None ]
|
||||||
|
|
||||||
preserve_hostname: false
|
preserve_hostname: false
|
||||||
manage_etc_hosts: {{ cloud_init_manage_etc_hosts | bool | lower }}
|
# `manage_etc_hosts` N'EST PAS ICI, ET C'EST VOULU (2026-08-25).
|
||||||
|
#
|
||||||
|
# Ce fragment le posait a `true`, pendant que `hosts_statiques` en posait un autre a
|
||||||
|
# `false`. Deux ecritures, deux fichiers, un desaccord — et dans `cloud.cfg.d` l'ordre
|
||||||
|
# est LEXICAL : `99-setops-hosts.cfg` (`-`, 0x2D) est lu avant `99_setops.cfg` (`_`,
|
||||||
|
# 0x5F). C'est donc celui-ci qui gagnait, et cloud-init reecrivait /etc/hosts a chaque
|
||||||
|
# demarrage — effacant le plancher de resolution en silence.
|
||||||
|
#
|
||||||
|
# Le defaut ne se voit qu'au redemarrage SUIVANT, et il se manifeste ailleurs : un `apt
|
||||||
|
# update` qui ne resout plus le cache, un certificat dont le nom ne pointe nulle part.
|
||||||
|
# Mesure du 2026-08-25 : les cinq machines du site redemarrees, /etc/hosts reduit aux
|
||||||
|
# entrees Debian par defaut, avec un `127.0.1.1 site-pki-01.chezlepro.ca` venu du
|
||||||
|
# `searchdomain` Proxmox — pas meme le bon domaine.
|
||||||
|
#
|
||||||
|
# LE ROLE QUI POSSEDE LE FICHIER DECIDE : `hosts_statiques` ecrit /etc/hosts, c'est donc
|
||||||
|
# lui qui dit si cloud-init peut y toucher. Quand il est inactif, rien ne l'interdit plus
|
||||||
|
# et cloud-init reprend son comportement par defaut — ce qui est le bon repli.
|
||||||
disable_root: {{ cloud_init_disable_root | bool | lower }}
|
disable_root: {{ cloud_init_disable_root | bool | lower }}
|
||||||
ssh_pwauth: {{ cloud_init_ssh_pwauth | bool | lower }}
|
ssh_pwauth: {{ cloud_init_ssh_pwauth | bool | lower }}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,22 @@
|
||||||
# On le desactive par un fragment plutot qu'en editant cloud.cfg : le gabarit reste
|
# 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
|
# intact, et `apt purge cloud-init` ou une remise a zero rend la machine a son etat
|
||||||
# d'origine.
|
# 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
|
- name: Empêcher cloud-init de réécrire le plancher à chaque démarrage
|
||||||
ansible.builtin.copy:
|
ansible.builtin.copy:
|
||||||
dest: /etc/cloud/cloud.cfg.d/99-setops-hosts.cfg
|
dest: /etc/cloud/cloud.cfg.d/zz-setops-hosts.cfg
|
||||||
content: |
|
content: |
|
||||||
# GÉNÉRÉ par Set-OPS (rôle hosts_statiques). NE PAS éditer à la main.
|
# 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.
|
# /etc/hosts est le PLANCHER DE RÉSOLUTION de l'écosystème, dérivé de l'inventaire.
|
||||||
|
|
@ -27,6 +40,31 @@
|
||||||
mode: "0644"
|
mode: "0644"
|
||||||
when: hosts_statiques_actif | bool
|
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.
|
# 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)
|
- name: Vérifier la présence des registres du plan (sur le nœud de contrôle)
|
||||||
ansible.builtin.stat:
|
ansible.builtin.stat:
|
||||||
|
|
@ -85,3 +123,76 @@
|
||||||
group: root
|
group: root
|
||||||
mode: "0644"
|
mode: "0644"
|
||||||
when: hosts_statiques_actif | bool
|
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'
|
||||||
|
|
|
||||||
17
roles/hosts_statiques/templates/hosts.debian.tmpl.j2
Normal file
17
roles/hosts_statiques/templates/hosts.debian.tmpl.j2
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
## template:jinja
|
||||||
|
# GABARIT MAÎTRE de /etc/hosts — GÉNÉRÉ par Set-OPS (rôle hosts_statiques).
|
||||||
|
#
|
||||||
|
# Cloud-init régénère /etc/hosts À CHAQUE DÉMARRAGE à partir de CE fichier lorsque
|
||||||
|
# `manage_etc_hosts` vaut vrai. Et il vaut vrai : Proxmox l'inscrit dans la user-data de
|
||||||
|
# son lecteur cloud-init, laquelle prime sur tout fragment de `/etc/cloud/cloud.cfg.d`.
|
||||||
|
# Aucun fragment ne peut donc gagner — ni l'ancien `99-setops-hosts.cfg`, ni son
|
||||||
|
# remplaçant `zz-`. Le seul endroit qui tienne est ici.
|
||||||
|
#
|
||||||
|
# Le contenu est le même que celui de /etc/hosts, à la ligne près : une seule source.
|
||||||
|
#
|
||||||
|
# CE QU'ON N'ÉCRIT PAS ICI : la ligne `127.0.1.1 {{ '{{fqdn}}' }} {{ '{{hostname}}' }}` du gabarit
|
||||||
|
# d'origine. Elle fait pointer le FQDN de la machine sur la boucle locale, avec le domaine
|
||||||
|
# du `searchdomain` Proxmox — `site-ops-01.chezlepro.ca` au lieu de
|
||||||
|
# `site-ops-01.genese.internal`. `hostname -f` rendrait alors un nom que personne ne peut
|
||||||
|
# vérifier, et c'est exactement le piège que le plancher documente déjà.
|
||||||
|
{% include 'hosts.j2' %}
|
||||||
|
|
@ -32,14 +32,39 @@
|
||||||
mode: "0644"
|
mode: "0644"
|
||||||
notify: Validate and reload PowerDNS
|
notify: Validate and reload PowerDNS
|
||||||
|
|
||||||
- name: Charger les registres applications et domaines (expositions)
|
# CHAQUE REGISTRE EST CHARGE POUR LUI-MEME (2026-08-25).
|
||||||
ansible.builtin.include_vars:
|
#
|
||||||
file: "{{ item }}"
|
# Ce `include_vars` listait les deux fichiers en dur et echouait si l'un manquait.
|
||||||
|
# `domaines.yml` declare les domaines PUBLICS, et un ecosysteme peut legitimement n'en
|
||||||
|
# avoir aucun — c'est le cas du SITE, dont tous les services sont internes.
|
||||||
|
#
|
||||||
|
# Le defaut est reste invisible tant que le site gardait `publier_expositions: false` ;
|
||||||
|
# il est apparu a la seconde ou on a voulu publier ses noms de service. C'est le MEME
|
||||||
|
# defaut que `hosts_statiques` a corrige le meme jour, dans les memes termes : un registre
|
||||||
|
# absent n'est pas une faute, il est simplement absent.
|
||||||
|
- name: Vérifier la présence des registres du plan (sur le nœud de contrôle)
|
||||||
|
ansible.builtin.stat:
|
||||||
|
path: "{{ item }}"
|
||||||
|
register: serveur_powerdns_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:
|
loop:
|
||||||
- "{{ setops_plan_dir }}/applications.yml"
|
- "{{ setops_plan_dir }}/applications.yml"
|
||||||
- "{{ setops_plan_dir }}/domaines.yml"
|
- "{{ setops_plan_dir }}/domaines.yml"
|
||||||
|
loop_control:
|
||||||
|
label: "{{ item | basename }}"
|
||||||
when: serveur_powerdns_publier_expositions | bool
|
when: serveur_powerdns_publier_expositions | bool
|
||||||
|
|
||||||
|
- name: Charger les registres applications et domaines (si présents)
|
||||||
|
ansible.builtin.include_vars:
|
||||||
|
file: "{{ item.item }}"
|
||||||
|
loop: "{{ serveur_powerdns_plan.results | default([]) }}"
|
||||||
|
loop_control:
|
||||||
|
label: "{{ item.item | basename }}"
|
||||||
|
when:
|
||||||
|
- serveur_powerdns_publier_expositions | bool
|
||||||
|
- item.stat.exists | default(false)
|
||||||
|
|
||||||
- name: Dériver les A d'exposition (FQDN publics -> edge qui les sert)
|
- name: Dériver les A d'exposition (FQDN publics -> edge qui les sert)
|
||||||
ansible.builtin.set_fact:
|
ansible.builtin.set_fact:
|
||||||
serveur_powerdns_expositions: >-
|
serveur_powerdns_expositions: >-
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,26 @@ $TTL {{ serveur_powerdns_ttl }}
|
||||||
@ IN NS {{ serveur_powerdns_nameserver }}.{{ serveur_powerdns_zone }}.
|
@ IN NS {{ serveur_powerdns_nameserver }}.{{ serveur_powerdns_zone }}.
|
||||||
{{ serveur_powerdns_nameserver }} IN A {{ hostvars[groups['serveur_powerdns'][0]].ansible_host | default('127.0.0.1') }}
|
{{ serveur_powerdns_nameserver }} IN A {{ hostvars[groups['serveur_powerdns'][0]].ansible_host | default('127.0.0.1') }}
|
||||||
|
|
||||||
{% for record in serveur_powerdns_records %}
|
{# LE PLAN L'EMPORTE SUR LE DEFAUT DU ROLE (2026-08-25).
|
||||||
|
|
||||||
|
`serveur_powerdns_records` porte un defaut de commodite : `dns IN CNAME ns1`. Le plan
|
||||||
|
du site, lui, expose `dns.genese.internal` pour `serveur_resolveur` — ce qui produit un
|
||||||
|
`dns IN A`. Un nom ne peut pas porter a la fois un CNAME et un A : `named-checkzone` a
|
||||||
|
refuse la zone (« CNAME and other data »), et PowerDNS ne l'a pas servie du tout.
|
||||||
|
|
||||||
|
La garde a bien fonctionne — elle a arrete une zone cassee avant qu'elle soit servie.
|
||||||
|
Mais elle laissait le DNS du site sans zone tant que la collision durait.
|
||||||
|
|
||||||
|
On ecarte donc le record statique quand le plan expose le meme nom. C'est le sens de
|
||||||
|
marche du depot : ce qui est DECLARE AU PLAN prime sur ce qu'un role suppose par
|
||||||
|
defaut. Les deux pointaient de toute facon vers la meme machine. #}
|
||||||
|
{% set _exposes = [] %}
|
||||||
|
{% if serveur_powerdns_publier_expositions | default(false) %}
|
||||||
|
{% for expo in serveur_powerdns_expositions | default([]) if expo.domaine == serveur_powerdns_zone %}
|
||||||
|
{% set _ = _exposes.append(expo.fqdn[:-(serveur_powerdns_zone | length + 1)]) %}
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% for record in serveur_powerdns_records if record.name not in _exposes %}
|
||||||
{{ record.name }} IN {{ record.type }} {{ record.value }}
|
{{ record.name }} IN {{ record.type }} {{ record.value }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -494,6 +494,20 @@ def expositions_des_applications(applications: dict, domaines: dict, edge: str |
|
||||||
edge_dom = conf.get("edge") or app.get("groupe")
|
edge_dom = conf.get("edge") or app.get("groupe")
|
||||||
if edge is not None and edge_dom != edge:
|
if edge is not None and edge_dom != edge:
|
||||||
continue
|
continue
|
||||||
|
# SANS DOMAINE PUBLIC DECLARE, LE DOMAINE EST CELUI QUE PORTE LE FQDN
|
||||||
|
# (2026-08-25). Meme repli que pour `edge` juste au-dessus, et il manquait.
|
||||||
|
#
|
||||||
|
# `domaine_parent` cherche le FQDN parmi les domaines PUBLICS. Le SITE n'en a
|
||||||
|
# aucun — tous ses noms sont internes — donc `dom` valait None, et le modele
|
||||||
|
# de zone PowerDNS, qui ne retient que les expositions dont le domaine EST sa
|
||||||
|
# zone, les ecartait toutes.
|
||||||
|
#
|
||||||
|
# Consequence mesuree le 2026-08-25 : `forge.genese.internal` et
|
||||||
|
# `pki.genese.internal` n'avaient aucun enregistrement A. Seul le plancher
|
||||||
|
# /etc/hosts savait les resoudre — or un service ne doit pas dependre d'un
|
||||||
|
# plancher pour etre joignable ; le plancher est un filet, pas le sol.
|
||||||
|
if not dom and "." in str(fqdn):
|
||||||
|
dom = str(fqdn).split(".", 1)[1]
|
||||||
resultat.append({
|
resultat.append({
|
||||||
"fqdn": fqdn, "application": app_id, "groupe": app.get("groupe"),
|
"fqdn": fqdn, "application": app_id, "groupe": app.get("groupe"),
|
||||||
"hote": app.get("hote"), "port": app.get("port"),
|
"hote": app.get("hote"), "port": app.get("port"),
|
||||||
|
|
|
||||||
|
|
@ -1143,6 +1143,60 @@ def preuve_parefeu_proxmox_sdn_seulement() -> tuple[bool, str]:
|
||||||
f"({len(cas)} cas evalues, dont un qui doit rendre VRAI).")
|
f"({len(cas)} cas evalues, dont un qui doit rendre VRAI).")
|
||||||
|
|
||||||
|
|
||||||
|
def preuve_un_seul_maitre_du_plancher() -> tuple[bool, str]:
|
||||||
|
"""Un seul role decide si cloud-init a le droit de reecrire /etc/hosts.
|
||||||
|
|
||||||
|
POURQUOI (2026-08-25). Deux roles ecrivaient un fragment dans `/etc/cloud/cloud.cfg.d`
|
||||||
|
et se contredisaient : `hosts_statiques` posait `manage_etc_hosts: false`,
|
||||||
|
`cloud_init` posait `true`. Or l'ordre y est LEXICAL — `99-setops-hosts.cfg` (`-`,
|
||||||
|
0x2D) est lu AVANT `99_setops.cfg` (`_`, 0x5F). C'est donc le `true` qui gagnait, et
|
||||||
|
cloud-init reecrivait /etc/hosts a chaque demarrage.
|
||||||
|
|
||||||
|
Le plancher de resolution disparaissait ainsi en silence, et le defaut ne se
|
||||||
|
manifestait qu'au redemarrage suivant, ailleurs : un `apt update` qui ne resout plus
|
||||||
|
le cache, un certificat dont le nom ne pointe nulle part. Mesure : les cinq machines
|
||||||
|
du site redemarrees le 2026-08-25 n'avaient plus que les entrees Debian par defaut.
|
||||||
|
|
||||||
|
Le role qui POSSEDE le fichier decide. Cette preuve refuse qu'un second s'en mele :
|
||||||
|
deux ecritures, meme d'accord aujourd'hui, sont un desaccord qui attend son heure.
|
||||||
|
"""
|
||||||
|
ecrivains: list[str] = []
|
||||||
|
for chemin in sorted((RACINE / "roles").rglob("*")):
|
||||||
|
if not chemin.is_file() or chemin.suffix not in (".yml", ".yaml", ".j2"):
|
||||||
|
continue
|
||||||
|
for ligne in chemin.read_text(encoding="utf-8", errors="replace").splitlines():
|
||||||
|
nu = ligne.strip()
|
||||||
|
if nu.startswith("#"):
|
||||||
|
continue # un commentaire explique, il n'ecrit pas
|
||||||
|
if re.match(r"^manage_etc_hosts\s*:", nu):
|
||||||
|
ecrivains.append(f"{chemin.relative_to(RACINE)} : {nu}")
|
||||||
|
if len(ecrivains) != 1:
|
||||||
|
return False, (
|
||||||
|
f"{len(ecrivains)} ecriture(s) de `manage_etc_hosts` dans les roles — il en "
|
||||||
|
f"faut exactement une, sinon l'ordre lexical de `cloud.cfg.d` tranche a notre "
|
||||||
|
f"place :\n - " + "\n - ".join(ecrivains or ["aucune : plus personne "
|
||||||
|
"n'empeche cloud-init d'effacer le plancher au demarrage"]))
|
||||||
|
|
||||||
|
# ET LE FRAGMENT NE SUFFIT PAS. Proxmox inscrit `manage_etc_hosts: true` dans la
|
||||||
|
# USER-DATA de son lecteur cloud-init, et la user-data prime sur tout `cloud.cfg.d`.
|
||||||
|
# Le plancher a donc ete efface a chaque demarrage malgre le fragment — d'abord sous
|
||||||
|
# le nom `99-`, puis sous `zz-` : renommer pour parler en dernier ne servait a rien,
|
||||||
|
# le dernier mot n'appartenant pas a ce repertoire.
|
||||||
|
#
|
||||||
|
# Ce que cloud-init regenere, il le regenere depuis `hosts.<distro>.tmpl`. Un role
|
||||||
|
# doit donc poser CE gabarit, sans quoi la correction n'en est pas une.
|
||||||
|
gabarit = [str(p.relative_to(RACINE)) for p in (RACINE / "roles").rglob("*")
|
||||||
|
if p.is_file() and "hosts.debian.tmpl" in p.name]
|
||||||
|
if not gabarit:
|
||||||
|
return False, (
|
||||||
|
"Aucun role ne pose `/etc/cloud/templates/hosts.debian.tmpl`. Le fragment "
|
||||||
|
"`cloud.cfg.d` seul ne protege rien : la user-data du lecteur cloud-init "
|
||||||
|
"(Proxmox y met `manage_etc_hosts: true`) prime sur ce repertoire. Le "
|
||||||
|
"plancher serait efface au prochain demarrage, sans erreur ni message.")
|
||||||
|
return True, (f"Un seul maitre du plancher — {ecrivains[0]} ; "
|
||||||
|
f"et le gabarit maitre est pose ({gabarit[0]}).")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def preuve_glossaire_enseigne() -> tuple[bool, str]:
|
def preuve_glossaire_enseigne() -> tuple[bool, str]:
|
||||||
"""Tout mot que le depot emploie devant l'exploitant est explique au glossaire.
|
"""Tout mot que le depot emploie devant l'exploitant est explique au glossaire.
|
||||||
|
|
@ -1486,6 +1540,8 @@ PREUVES: list[dict] = [
|
||||||
"func": preuve_ordre_des_integrations},
|
"func": preuve_ordre_des_integrations},
|
||||||
{"id": "P45", "titre": "Pare-feu Proxmox : arme sur les VNet SDN, jamais ailleurs",
|
{"id": "P45", "titre": "Pare-feu Proxmox : arme sur les VNet SDN, jamais ailleurs",
|
||||||
"refs": [], "func": preuve_parefeu_proxmox_sdn_seulement},
|
"refs": [], "func": preuve_parefeu_proxmox_sdn_seulement},
|
||||||
|
{"id": "P46", "titre": "Plancher /etc/hosts : un seul role en decide",
|
||||||
|
"refs": [], "func": preuve_un_seul_maitre_du_plancher},
|
||||||
{"id": "P43", "titre": "Frontiere : le devis voit les machines du site", "refs": [],
|
{"id": "P43", "titre": "Frontiere : le devis voit les machines du site", "refs": [],
|
||||||
"func": preuve_devis_frontiere_du_site},
|
"func": preuve_devis_frontiere_du_site},
|
||||||
{"id": "P33", "titre": "Aucune collision de port entre roles co-localises", "refs": [],
|
{"id": "P33", "titre": "Aucune collision de port entre roles co-localises", "refs": [],
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue