D-71 : PKI et DNS debout avant tout, et la zone inverse manquait

Contrainte de l'exploitant apres avoir vu backup-01 et collab-01 crees avant
l'AC et le DNS. Ma premiere reponse etait incomplete : un clone est bien
inerte, mais un echec a la 12e VM coute 40 minutes sans rien deployer, et le
journal donne l'impression que le moteur ignore ses couches.

Mesure avant de coder :
- enregistrements A : DEJA derives du plan (zone generee depuis hotes_actifs)
- zone inverse / PTR : n'existe NULLE PART, aucun role ne touche in-addr.arpa
- ordre d'amorcage : aucun, deployer-tout est par couches

Le premier point a reduit le travail de moitie — j'allais ecrire un enrolement
DNS par hote alors que la zone directe etait deja correcte.

Zone inverse derivee du supernet (27.10.in-addr.arpa), PTR issus de la MEME
source que les A : pas d'endroit ou elles puissent diverger. Vide si le
supernet n'est pas un /16.

_amorcer-socle monte l'AC puis le DNS completement avant deployer-tout ; les
deux derives de applications.<app>.hote, dans un ordre causal et non
alphabetique. Deux exceptions assumees : l'AC s'auto-signe, le DNS pose son
propre enregistrement.

P10 a attrape un handler que je venais d'inventer (Recharger PowerDNS au lieu
de Validate and reload PowerDNS) avant tout deploiement.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Daniel Allaire 2026-08-08 15:29:57 -04:00
parent 872d590031
commit 1295eeaf4e
11 changed files with 227 additions and 2 deletions

View file

@ -1,5 +1,47 @@
# CHANGELOG — Set-OPS # CHANGELOG — Set-OPS
## 2026-08-08 — D-71 : PKI et DNS debout avant tout le reste, et la zone inverse
Contrainte posée par l'exploitant en voyant `backup-01` et `collab-01` créés avant l'AC et
le DNS : **une PKI et un DNS fonctionnels avant toute chose** ; puis par VM, socle →
enrôlement PKI → enregistrement DNS (A et PTR).
Ma première réponse était incomplète. J'avais expliqué qu'un clone est inerte et que
`site.yml` respecte bien les couches — c'est exact, mais ça esquivait deux points justes :
un échec de création à la douzième VM coûte quarante minutes sans rien déployer, et un
journal qui montre `backup-01` en tête donne l'impression que le moteur ignore ses propres
couches.
**Ce qui manquait vraiment, mesuré avant de coder :**
```
enregistrements A → DEJA derives du plan (zone generee depuis `hotes_actifs`)
zone inverse / PTR → n'existe NULLE PART — aucun role ne touche `in-addr.arpa`
ordre d'amorcage → aucun : `deployer-tout` est par couches, pas par hote
```
Le premier point a réduit le travail de moitié : je m'apprêtais à écrire un enrôlement DNS
par hôte alors que la zone directe se dérivait déjà correctement.
**La zone inverse** est dérivée du supernet, comme le reste : un `/16` `10.(10+index).0.0`
donne `(10+index).10.in-addr.arpa``27.10.in-addr.arpa` ici. Les PTR viennent de la
**même source** que les A (`hotes_actifs` et son `ansible_host`) : deux zones alimentées
par une seule vérité, donc pas d'endroit où elles puissent diverger. Vide si le supernet
n'est pas un `/16` — mieux vaut pas de zone inverse qu'une zone fausse.
**`_amorcer-socle`** monte l'AC puis le DNS **complètement**, hôte par hôte, avant
`deployer-tout`. Les deux se dérivent de `applications.<app>.hote` ; l'ordre entre eux
n'est pas alphabétique mais causal — le DNS a besoin d'un certificat, l'autorité n'a besoin
de personne.
**Deux exceptions structurelles, nommées plutôt que découvertes à l'exécution** : l'AC
s'auto-signe, et le DNS pose son propre enregistrement. La règle « socle → PKI → DNS » ne
peut pas s'appliquer à ceux qui la rendent possible.
Au passage, le harnais a attrapé une faute que je venais d'introduire : j'avais inventé un
handler `Recharger PowerDNS` qui n'existe pas — le rôle écoute `Validate and reload
PowerDNS`. P10 l'a nommée avant tout déploiement.
## 2026-08-08 — Une question de l'exploitant trouve un trou dans P31, une heure après ## 2026-08-08 — Une question de l'exploitant trouve un trou dans P31, une heure après
« Pourquoi pas `make myDay` ? » — la cible existe, c'est un alias strict de « Pourquoi pas `make myDay` ? » — la cible existe, c'est un alias strict de

View file

@ -594,11 +594,29 @@ reconstruire: _instance-requise ## Reconstruit un ecosysteme depuis zero : VM pu
fi; \ fi; \
$(MAKE) flotte-creer CONFIRMER=true; \ $(MAKE) flotte-creer CONFIRMER=true; \
$(MAKE) _attendre-flotte; \ $(MAKE) _attendre-flotte; \
$(MAKE) _amorcer-socle; \
$(MAKE) deployer-tout CONFIRMER=true $(MAKE) deployer-tout CONFIRMER=true
# « Go ahead, make my day. » — LE bouton rouge : alias de reconstruire (Clint Eastwood). # « Go ahead, make my day. » — LE bouton rouge : alias de reconstruire (Clint Eastwood).
# Cree toutes les VM puis deploie tout, en une commande. Garde CONFIRMER=true. # Cree toutes les VM puis deploie tout, en une commande. Garde CONFIRMER=true.
.PHONY: myDay .PHONY: myDay
# « Une PKI et un DNS fonctionnels avant toute chose » — contrainte d'architecture du
# 2026-08-08. `deployer-tout` deroule par COUCHES : tous les hotes recoivent le socle,
# puis tous recoivent la couche suivante. C'est correct, mais ca laisse chaque VM
# reclamer un certificat a une autorite qui n'est pas encore debout — et l'echec se lit
# alors comme un defaut du role, pas comme un defaut d'ordre.
#
# On monte donc l'AC puis le DNS COMPLETEMENT d'abord, hote par hote (`make deployer`
# ordonne deja les groupes d'un hote par couches). Les deux se DERIVENT du plan :
# deplacer l'autorite deplace l'amorcage avec elle.
_amorcer-socle: ansible-runtime _instance-requise
@set -e; \
for h in $$(python3 scripts/socle_amorcage.py); do \
printf '\n=== Amorcage du socle : %s ===\n' "$$h"; \
$(MAKE) deployer HOTE="$$h"; \
done; \
printf '\nPKI et DNS debout : le reste de la flotte peut monter.\n'
myDay: reconstruire ## Alias strict de `reconstruire` (meme cible, memes gardes) myDay: reconstruire ## Alias strict de `reconstruire` (meme cible, memes gardes)
deployer-groupe: ## Deploie un seul groupe sur toute la flotte — GROUPE=<groupe> deployer-groupe: ## Deploie un seul groupe sur toute la flotte — GROUPE=<groupe>

View file

@ -43,7 +43,7 @@
| P28 | Pools Proxmox : un par tenant, sans collision | AFF-110 | ✅ OK | CONFORME : 2 pool(s) Proxmox, 28 VM placee(s), aucun nom ni VMID en collision. | | P28 | Pools Proxmox : un par tenant, sans collision | AFF-110 | ✅ OK | CONFORME : 2 pool(s) Proxmox, 28 VM placee(s), aucun nom ni VMID en collision. |
| P29 | Authentification : chaque role declare sa position | AFF-111 | ✅ OK | 23 role(s) serveur declares (interne-sans-auth 2, ldap-direct 2, sans-auth-humaine 12, socle-identite 2, web-sso 5) ; 2 lacune(s) nommee(s) : serveur_loki, serv | | P29 | Authentification : chaque role declare sa position | AFF-111 | ✅ OK | 23 role(s) serveur declares (interne-sans-auth 2, ldap-direct 2, sans-auth-humaine 12, 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, 2 zone(s), 12 VNet(s), 12 sous-reseau(x), aucune collision. | | P30 | SDN EVPN : zones, VNets et sous-reseaux derives | AFF-112 | ✅ OK | CONFORME : SDN EVPN, 2 zone(s), 12 VNet(s), 12 sous-reseau(x), aucune collision. |
| P31 | Documentation : tout ce que le depot FAIT est nomme | — | ✅ OK | 36 scripts expliques et atteignables, 87 cibles make documentees, 54 roles avec README. | | P31 | Documentation : tout ce que le depot FAIT est nomme | — | ✅ OK | 37 scripts expliques et atteignables, 87 cibles make documentees, 54 roles avec README. |
## Couverture des affirmations ✅ du registre ## Couverture des affirmations ✅ du registre

View file

@ -23,7 +23,7 @@ code + les README de rôles). Cette page comble ces deux trous.
| **Ordre de déploiement** | `docs/couches-deploiement.yml` (couches) + `docs/dependances-groupes.yml` (graphe) → `playbooks/site.yml` (**généré**, `make site`) | | **Ordre de déploiement** | `docs/couches-deploiement.yml` (couches) + `docs/dependances-groupes.yml` (graphe) → `playbooks/site.yml` (**généré**, `make site`) |
| **Conformité du déployé** | `docs/devis-services.md` — les **cinq devis de service** (`make identite-plan`, `certificats-plan`, `expositions-plan`, `postgresql-plan`, `courriel-plan`). Répondent à ce que `make prouver` ne demande jamais : *ce qui tourne correspond-il à ce qui est déclaré ?* | | **Conformité du déployé** | `docs/devis-services.md` — les **cinq devis de service** (`make identite-plan`, `certificats-plan`, `expositions-plan`, `postgresql-plan`, `courriel-plan`). Répondent à ce que `make prouver` ne demande jamais : *ce qui tourne correspond-il à ce qui est déclaré ?* |
| **Preuve / recette** | `docs/audit/affirmations.md` (registre), `make prouver``docs/audit/preuve-<date>.md`**statique** : lit le dépôt, aucun appel réseau ; la conformité du déployé est l'affaire des devis de service (ligne au-dessus), `docs/audit/plan-de-recette.md` (**généré** du wiki), `docs/audit/protocole-operateur-independant.md` | | **Preuve / recette** | `docs/audit/affirmations.md` (registre), `make prouver``docs/audit/preuve-<date>.md`**statique** : lit le dépôt, aucun appel réseau ; la conformité du déployé est l'affaire des devis de service (ligne au-dessus), `docs/audit/plan-de-recette.md` (**généré** du wiki), `docs/audit/protocole-operateur-independant.md` |
| **Décisions d'architecture** | `docs/decisions-architecture.md`**67 décisions en vigueur** (D-01 → D-70, 3 renversées), pourquoi, où lire le détail, et ce qui les garde ; plus les **décisions renversées** et leur cause | | **Décisions d'architecture** | `docs/decisions-architecture.md`**68 décisions en vigueur** (D-01 → D-71, 3 renversées), pourquoi, où lire le détail, et ce qui les garde ; plus les **décisions renversées** et leur cause |
| **SDN / routage** | `docs/sdn-evpn.md` — décision du 2026-08-02 : le routage inter-zone passe des commutateurs aux hyperviseurs (zones EVPN = VRF). **Non éprouvé** : spike avant génération | | **SDN / routage** | `docs/sdn-evpn.md` — décision du 2026-08-02 : le routage inter-zone passe des commutateurs aux hyperviseurs (zones EVPN = VRF). **Non éprouvé** : spike avant génération |
| **Migration de tenant** | `docs/migration-tenant.md` — recette en 8 étapes, machine à états, gardes ; le receveur se construit **avant** tout gel | | **Migration de tenant** | `docs/migration-tenant.md` — recette en 8 étapes, machine à états, gardes ; le receveur se construit **avant** tout gel |
| **Exploitation courante** | `docs/runbooks-exploitation.md`, `docs/intrants-communs.md`, `docs/intrants-base-gui-conception.md`, `docs/theme-forgejo-hors-flotte.md` | | **Exploitation courante** | `docs/runbooks-exploitation.md`, `docs/intrants-communs.md`, `docs/intrants-base-gui-conception.md`, `docs/theme-forgejo-hors-flotte.md` |

View file

@ -108,6 +108,7 @@ sont les seules vérifiables.
| **D-68** | On **écrit, puis on relit et on compare** — quelle que soit l'interface ; on choisit celle dont le chemin de **lecture** parle le même langage que le chemin d'**écriture** | « toujours préférer l'API » n'aurait prédit aucune des pannes du 2026-08-08 : sur six familles de défauts, deux venaient d'un CLI, une d'un module Ansible (`ldap_entry` crée sans jamais modifier), une d'un `grep` de fichier, une de la précédence Ansible, une de mon comparateur. Le facteur commun est d'avoir écrit sans relire. Et la plupart de la flotte n'a **pas** d'API — Postfix, Dovecot, nginx, slapd, nftables : `postconf -h` / `postconf -e` sont symétriques, c'est tout ce qu'on demande | `devis-services.md` | les 5 devis | | **D-68** | On **écrit, puis on relit et on compare** — quelle que soit l'interface ; on choisit celle dont le chemin de **lecture** parle le même langage que le chemin d'**écriture** | « toujours préférer l'API » n'aurait prédit aucune des pannes du 2026-08-08 : sur six familles de défauts, deux venaient d'un CLI, une d'un module Ansible (`ldap_entry` crée sans jamais modifier), une d'un `grep` de fichier, une de la précédence Ansible, une de mon comparateur. Le facteur commun est d'avoir écrit sans relire. Et la plupart de la flotte n'a **pas** d'API — Postfix, Dovecot, nginx, slapd, nftables : `postconf -h` / `postconf -e` sont symétriques, c'est tout ce qu'on demande | `devis-services.md` | les 5 devis |
| **D-69** | Sur Keycloak : **l'API pour toute map ou collection** (`smtpServer`, `attributes`, `config`), `kcadm` pour les scalaires et les créations | `kcadm -s` sur une map accepte la commande, **sort en succès et n'écrit rien** — mesuré deux fois le 2026-08-08 (`smtpServer` resté vide après deux déploiements verts, puis `post.logout.redirect.uris`). Le CLI reste préféré ailleurs : c'est le vocabulaire de la documentation du produit, donc lisible sans IA | `roles/serveur_keycloak/tasks/` | `make identite-plan` | | **D-69** | Sur Keycloak : **l'API pour toute map ou collection** (`smtpServer`, `attributes`, `config`), `kcadm` pour les scalaires et les créations | `kcadm -s` sur une map accepte la commande, **sort en succès et n'écrit rien** — mesuré deux fois le 2026-08-08 (`smtpServer` resté vide après deux déploiements verts, puis `post.logout.redirect.uris`). Le CLI reste préféré ailleurs : c'est le vocabulaire de la documentation du produit, donc lisible sans IA | `roles/serveur_keycloak/tasks/` | `make identite-plan` |
| **D-70** | La documentation **dit et explique tout ce que le dépôt fait** — et l'exigence est **outillée**, pas seulement énoncée | une exigence qu'on n'outille pas pourrit en silence : la carte annonçait « 28 décisions » quand il y en avait 66, et disait les accès « non construits » alors qu'ils tournaient en production. **P31** garde le couvert — chaque script s'explique et reste atteignable, chaque cible `make` porte son aide (sauf les internes préfixées `_`), chaque rôle a son README. Elle ne garde **pas** la qualité du « pourquoi » : ça se juge en revue, et ça vit dans `CHANGELOG.md` et ici | `devis-services.md`, `CHANGELOG.md` | **P31** | | **D-70** | La documentation **dit et explique tout ce que le dépôt fait** — et l'exigence est **outillée**, pas seulement énoncée | une exigence qu'on n'outille pas pourrit en silence : la carte annonçait « 28 décisions » quand il y en avait 66, et disait les accès « non construits » alors qu'ils tournaient en production. **P31** garde le couvert — chaque script s'explique et reste atteignable, chaque cible `make` porte son aide (sauf les internes préfixées `_`), chaque rôle a son README. Elle ne garde **pas** la qualité du « pourquoi » : ça se juge en revue, et ça vit dans `CHANGELOG.md` et ici | `devis-services.md`, `CHANGELOG.md` | **P31** |
| **D-71** | **Une PKI et un DNS fonctionnels avant toute chose** ; puis, par VM : socle → enrôlement PKI → enregistrement DNS (A **et** PTR) | `deployer-tout` déroule par COUCHES — correct, mais chaque VM réclame alors un certificat à une autorité pas encore debout, et l'échec se lit comme un défaut du rôle et non d'ordre. Les deux hôtes d'amorçage se **dérivent** de `applications.<step_ca\|powerdns>.hote` : déplacer l'autorité déplace l'amorçage. **Deux exceptions structurelles assumées** — l'AC s'auto-signe, le DNS pose son propre enregistrement | `Makefile` `_amorcer-socle`, `scripts/socle_amorcage.py` | — |
--- ---

View file

@ -0,0 +1,55 @@
---
serveur_powerdns_packages:
- pdns-server
- pdns-backend-bind
- bind9-utils
- dnsutils
serveur_powerdns_service_name: "pdns"
serveur_powerdns_zone_directory: "/etc/powerdns/zones"
serveur_powerdns_bind_config: "/etc/powerdns/bindbackend.conf"
serveur_powerdns_zone: "{{ domaine_interne }}"
serveur_powerdns_nameserver: "ns1"
serveur_powerdns_contact: "hostmaster.{{ domaine_interne }}"
serveur_powerdns_ttl: 3600
serveur_powerdns_serial: 2026062201
serveur_powerdns_soa_refresh: 3600
serveur_powerdns_soa_retry: 900
serveur_powerdns_soa_expire: 1209600
serveur_powerdns_soa_minimum: 3600
# L'adresse du tenant SEULEMENT, pas `0.0.0.0` : lier toutes les interfaces occupe aussi
# `127.0.0.1:53`, ou un resolveur recursif local voudrait s'installer. Ce n'est pas un
# probleme theorique — c'est la raison pour laquelle `client_unbound` exempte cet hote.
# PowerDNS reste autoritatif pur : il repond pour la zone souveraine et REFUSE le reste.
serveur_powerdns_listen_addresses:
- "{{ ansible_host | default('0.0.0.0') }}"
serveur_powerdns_allow_axfr_ips: []
# Enregistrements additionnels geres explicitement.
serveur_powerdns_records:
- name: "dns"
type: "CNAME"
value: "ns1.{{ serveur_powerdns_zone }}."
# Groupes dont les hotes actifs avec ansible_host seront ajoutes a la zone.
serveur_powerdns_inventory_groups:
- hotes_actifs
# Publier des A pour les FQDN d'exposition (champ 'expose' des applications) vers l'edge
# qui les sert (domaines.edge). Ferme la boucle : declarer 'expose' -> vhost + cert + DNS.
serveur_powerdns_publier_expositions: true
serveur_powerdns_expositions: []
# Zone INVERSE — DERIVEE du supernet, jamais ecrite. Un /16 `10.(10+index).0.0` donne
# `(10+index).10.in-addr.arpa`. Vide si le supernet n'est pas connu ou n'est pas un /16 :
# mieux vaut pas de zone inverse qu'une zone fausse.
#
# Pourquoi elle manquait. Rien dans le moteur ne touchait `in-addr.arpa` — mesure du
# 2026-08-08. Un hote pouvait donc etre resolu par son nom et rester anonyme a l'envers,
# ce que la plupart des services de courriel et de journalisation reprochent en silence.
serveur_powerdns_zone_inverse: >-
{{ (setops_supernet | default('') | ansible.utils.ipaddr('prefix') | int == 16)
| ternary(
(setops_supernet | default('0.0.0.0/0')).split('.')[1] ~ '.'
~ (setops_supernet | default('0.0.0.0/0')).split('.')[0] ~ '.in-addr.arpa',
'') }}

View file

@ -39,3 +39,17 @@ serveur_powerdns_inventory_groups:
# qui les sert (domaines.edge). Ferme la boucle : declarer 'expose' -> vhost + cert + DNS. # qui les sert (domaines.edge). Ferme la boucle : declarer 'expose' -> vhost + cert + DNS.
serveur_powerdns_publier_expositions: true serveur_powerdns_publier_expositions: true
serveur_powerdns_expositions: [] serveur_powerdns_expositions: []
# Zone INVERSE — DERIVEE du supernet, jamais ecrite. Un /16 `10.(10+index).0.0` donne
# `(10+index).10.in-addr.arpa`. Vide si le supernet n'est pas connu ou n'est pas un /16 :
# mieux vaut pas de zone inverse qu'une zone fausse.
#
# Pourquoi elle manquait. Rien dans le moteur ne touchait `in-addr.arpa` — mesure du
# 2026-08-08. Un hote pouvait donc etre resolu par son nom et rester anonyme a l'envers,
# ce que la plupart des services de courriel et de journalisation reprochent en silence.
serveur_powerdns_zone_inverse: >-
{{ (setops_supernet | default('') | ansible.utils.ipaddr('prefix') | int == 16)
| ternary(
(setops_supernet | default('0.0.0.0/0')).split('.')[1] ~ '.'
~ (setops_supernet | default('0.0.0.0/0')).split('.')[0] ~ '.in-addr.arpa',
'') }}

View file

@ -56,6 +56,18 @@
mode: "0644" mode: "0644"
notify: Validate and reload PowerDNS notify: Validate and reload PowerDNS
# Meme source que la zone directe : `hotes_actifs` et son `ansible_host`. Deux zones
# alimentees par une seule verite — l'endroit ou elles divergeraient n'existe pas.
- name: Générer la zone DNS inverse depuis l'inventaire
ansible.builtin.template:
src: zone-inverse.db.j2
dest: "{{ serveur_powerdns_zone_directory }}/{{ serveur_powerdns_zone_inverse }}.zone"
owner: root
group: root
mode: "0644"
when: serveur_powerdns_zone_inverse | length > 0
notify: Validate and reload PowerDNS
- name: Activer et démarrer PowerDNS - name: Activer et démarrer PowerDNS
ansible.builtin.systemd: ansible.builtin.systemd:
name: "{{ serveur_powerdns_service_name }}" name: "{{ serveur_powerdns_service_name }}"

View file

@ -2,3 +2,9 @@ zone "{{ serveur_powerdns_zone }}" {
type master; type master;
file "{{ serveur_powerdns_zone_directory }}/{{ serveur_powerdns_zone }}.zone"; file "{{ serveur_powerdns_zone_directory }}/{{ serveur_powerdns_zone }}.zone";
}; };
{% if serveur_powerdns_zone_inverse %}
zone "{{ serveur_powerdns_zone_inverse }}" {
type master;
file "{{ serveur_powerdns_zone_directory }}/{{ serveur_powerdns_zone_inverse }}.zone";
};
{% endif %}

View file

@ -0,0 +1,24 @@
; Zone INVERSE — generee par Set-OPS (role serveur_powerdns). Ne pas editer a la main.
;
; Le nom de la zone est DERIVE du supernet du tenant, comme tout le reste : un /16
; 10.(10+index).0.0 donne (10+index).10.in-addr.arpa. Rien n'est ecrit.
;
; Les PTR viennent de la MEME source que les A — `hotes_actifs` et son `ansible_host`.
; Deux zones alimentees par une seule verite : c'est ce qui empeche l'endroit ou elles
; divergent d'exister.
$TTL {{ serveur_powerdns_ttl }}
@ IN SOA {{ serveur_powerdns_nameserver }}.{{ serveur_powerdns_zone }}. {{ serveur_powerdns_contact }}. (
{{ serveur_powerdns_serial }} ; serial
{{ serveur_powerdns_soa_refresh }} ; refresh
{{ serveur_powerdns_soa_retry }} ; retry
{{ serveur_powerdns_soa_expire }} ; expire
{{ serveur_powerdns_soa_minimum }} ) ; minimum
@ IN NS {{ serveur_powerdns_nameserver }}.{{ serveur_powerdns_zone }}.
{% for host_name in groups.get('hotes_actifs', []) | sort %}
{% set ip = hostvars[host_name].ansible_host | default('') %}
{% if ip %}
{{ ip.split('.')[3] }}.{{ ip.split('.')[2] }} IN PTR {{ host_name }}.{{ serveur_powerdns_zone }}.
{% endif %}
{% endfor %}

53
scripts/socle_amorcage.py Executable file
View file

@ -0,0 +1,53 @@
#!/usr/bin/env python3
"""Nomme les hotes qui doivent etre DEBOUT avant tous les autres, derives du plan.
Contrainte d'architecture posee le 2026-08-08 : « une PKI et un DNS fonctionnels avant
toute chose ». Sans eux, chaque VM qui monte reclame un certificat a une autorite absente
et un nom a un annuaire muet et l'echec se lit comme un defaut du role, pas comme un
defaut d'ordre.
Les hotes ne sont PAS ecrits : ils se derivent de `applications.<app>.hote`. Deplacer
l'autorite dans le plan deplace l'amorcage avec elle.
python3 scripts/socle_amorcage.py # un nom d'hote par ligne, dans l'ordre
"""
from __future__ import annotations
import os
import sys
from pathlib import Path
import yaml
RACINE = Path(__file__).resolve().parent.parent
# L'ordre compte et n'est pas alphabetique : le DNS a besoin d'un certificat, l'autorite
# n'a besoin de personne. Elle passe donc devant — c'est la seule dependance reelle
# entre les deux.
APPLICATIONS = ["step_ca", "powerdns"]
def main() -> int:
base = Path(os.environ.get("SETOPS_INSTANCE") or (RACINE / "instance"))
plan = base / "plan" / "applications.yml"
if not plan.exists():
print(f"Plan introuvable : {plan}", file=sys.stderr)
return 2
apps = (yaml.safe_load(plan.read_text(encoding="utf-8")) or {}).get("applications") or {}
vus, sortie = set(), []
for nom in APPLICATIONS:
hote = (apps.get(nom) or {}).get("hote")
if not hote:
print(f"L'application « {nom} » n'est pas au plan : impossible d'amorcer le "
f"socle sans elle.", file=sys.stderr)
return 2
if hote not in vus: # PKI et DNS peuvent partager un hote
vus.add(hote)
sortie.append(hote)
print("\n".join(sortie))
return 0
if __name__ == "__main__":
raise SystemExit(main())