2026-06-24 20:17:46 -04:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
"""Gere les hotes d'un inventaire Ansible statique Set-OPS."""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import argparse
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
import yaml
|
|
|
|
|
|
|
|
|
|
from inventory_rules import (
|
plan : l ecriture des registres devient atomique — tout, ou rien
`path.open("w")` TRONQUE avant d ecrire : entre les deux, le fichier est vide.
Une exception dans yaml.safe_dump, un disque plein, un Ctrl-C, et
instance/plan/serveurs.yml reste mutile.
L asymetrie fait la gravite : hosts.yml se regenere d un
make instancier-appliquer, le PLAN ne se regenere de rien. C est la source
unique de verite. Git est le filet, mais encore faut-il savoir qu on est tombe.
TREIZE SITES, UNE SEULE FONCTION
Le defaut n etait pas dans le GUI seul : douze sites dans sept fichiers, dont
les miroirs CLI des MEMES registres. Corriger le GUI seul aurait recree la
divergence que P41 garde depuis les neuf resolutions d instance. La fonction
vit donc dans inventory_rules.py, que les sept importaient deja. Une source,
pas douze.
TROIS DETAILS QUI FONT LA DIFFERENCE ENTRE « CA MARCHE » ET « CA TIENT »
temporaire dans le MEME dossier os.replace n est atomique qu au sein d un
meme systeme de fichiers ; un /tmp sur une
autre partition casserait la garantie sans
rien dire
fsync AVANT le rename sinon le renommage peut atteindre le disque
avant le contenu : au retour d une coupure
brutale, un fichier neuf et VIDE — le defaut
qu on ferme, deplace d un cran
report des droits mkstemp cree en 0600, le plan est en 0664 et
doit rester lisible par le groupe sur les
runners
LE TEST PORTE SON PROPRE CONTROLE NEGATIF
scripts/tests/test_ecriture_atomique.py rejoue D ABORD l ancienne forme et
verifie qu elle DETRUIT. Sans ce controle, « le fichier est intact » ne
prouverait rien — il pourrait l etre parce que rien n a ete ecrit du tout. Une
garantie qu on n a jamais vue echouer n est pas une garantie, c est une
habitude.
Branche sur P02, dont le titre annoncait « inventory_host » alors qu il lance
maintenant trois tests. Corrige au passage.
LA VOUTE DU GUI : VERIFIEE, PAS DE DEFAUT
Le soupcon etait qu executer_flux pose ANSIBLE_VAULT_PASSWORD_FILE (un seul mot
de passe) alors que creer une VM ouvre DEUX voutes depuis « une voute, une cle ».
Eprouve contre deux voutes JETABLES a mots de passe distincts — jamais les
vraies. Les deux variables se CUMULENT : Ansible essaie tous les secrets, et un
PASSWORD_FILE errone n empeche rien. Confirme en sondant l environnement qu une
recette make recoit reellement : le mot de passe saisi ET les cinq cles
calculees par voutes.py.
Ce qui sauve ce chemin n est donc pas le mot de passe saisi, c est
l IDENTITY_LIST que make pose par-dessus. Chacun couvre ce que l autre ne
couvre pas — le PASSWORD_FILE sert le runner qui n a que sa cle, l IDENTITY_LIST
le poste qui les a toutes. Ecrit au-dessus du code, pour que personne ne
« simplifie » en retirant l un des deux.
make prouver : CONFORME, 59 OK, 0 echec, 1 saute.
make instancier : DIFF VIDE, quatre registres relus, droits 664 preserves.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 14:57:57 -04:00
|
|
|
ecriture_atomique,
|
dependances : un ecosysteme minimal n'est pas un ecosysteme incomplet
Le controle de dependances a REFUSE le deploiement de patient 0 : client_journal requiert
serveur_loki, client_metrique requiert serveur_prometheus, serveur_forgejo requiert
serveur_postgresql et serveur_postfix. Quatre refus, une seule racine — le moteur suppose
que tout ecosysteme porte tous les services. Apres les intrants (P32) et les bases (P35),
troisieme manifestation en cinq jours. Et le mur que rencontrerait toute offre plus petite
que l'ecosysteme de reference.
UNE INTEGRATION UNIVERSELLE A BESOIN D'UN INTERLOCUTEUR. « Tout hote est mesure » est vrai
dans un ecosysteme qui porte un Prometheus ; ailleurs, la meme phrase pose sur chaque
machine un client qui n'a personne a qui parler. La regle est desormais DERIVEE : le
service central d'une integration est celui que le registre des dependances lui donne
deja. Rien de neuf a tenir a jour, donc rien de neuf a oublier.
Chezlepro -> diff VIDE (tous ses services existent, rien ne change)
patient 0 -> client_backup, client_pki, client_unbound
UNE EXIGENCE N'EST PAS TOUJOURS ABSOLUE. Deux notions manquaient au registre :
`sauf_si` l'exigence tombe sous condition (Forgejo + SQLite)
`utilise_si_present` un agrement, jamais bloquant (Forgejo notifie SI un MTA existe)
Les confondre obligeait une forge a deployer une pile courriel entiere pour exister.
ET LA LECON D'HIER A SERVI : trois lecteurs avaient besoin le meme jour de lire une
variable d'instance (P35, les clauses sauf_si, le generateur). Trois copies auraient
recommence ce qu'on venait de refermer. Il y en a UNE, dans inventory_rules.
make verifier 41/41 ; make ci 41/41 ; inventaire de Chezlepro identique.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 14:54:00 -04:00
|
|
|
variable_effective,
|
2026-06-24 20:17:46 -04:00
|
|
|
GROUPE_HOTES_ACTIFS,
|
|
|
|
|
GROUPE_HOTES_PLANIFIES,
|
|
|
|
|
GROUPES_ETAT_HOTE,
|
|
|
|
|
charger_dependances,
|
|
|
|
|
est_groupe_operationnel,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def charger_inventaire(path: Path) -> dict:
|
|
|
|
|
if not path.exists():
|
|
|
|
|
return {"all": {"children": {}}}
|
|
|
|
|
|
|
|
|
|
with path.open("r", encoding="utf-8") as inventory_file:
|
|
|
|
|
data = yaml.safe_load(inventory_file) or {}
|
|
|
|
|
|
|
|
|
|
if not isinstance(data, dict):
|
|
|
|
|
raise ValueError(f"{path} ne contient pas une table YAML.")
|
|
|
|
|
|
|
|
|
|
data.setdefault("all", {})
|
|
|
|
|
data["all"].setdefault("children", {})
|
|
|
|
|
return data
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def ecrire_inventaire(path: Path, data: dict) -> None:
|
plan : l ecriture des registres devient atomique — tout, ou rien
`path.open("w")` TRONQUE avant d ecrire : entre les deux, le fichier est vide.
Une exception dans yaml.safe_dump, un disque plein, un Ctrl-C, et
instance/plan/serveurs.yml reste mutile.
L asymetrie fait la gravite : hosts.yml se regenere d un
make instancier-appliquer, le PLAN ne se regenere de rien. C est la source
unique de verite. Git est le filet, mais encore faut-il savoir qu on est tombe.
TREIZE SITES, UNE SEULE FONCTION
Le defaut n etait pas dans le GUI seul : douze sites dans sept fichiers, dont
les miroirs CLI des MEMES registres. Corriger le GUI seul aurait recree la
divergence que P41 garde depuis les neuf resolutions d instance. La fonction
vit donc dans inventory_rules.py, que les sept importaient deja. Une source,
pas douze.
TROIS DETAILS QUI FONT LA DIFFERENCE ENTRE « CA MARCHE » ET « CA TIENT »
temporaire dans le MEME dossier os.replace n est atomique qu au sein d un
meme systeme de fichiers ; un /tmp sur une
autre partition casserait la garantie sans
rien dire
fsync AVANT le rename sinon le renommage peut atteindre le disque
avant le contenu : au retour d une coupure
brutale, un fichier neuf et VIDE — le defaut
qu on ferme, deplace d un cran
report des droits mkstemp cree en 0600, le plan est en 0664 et
doit rester lisible par le groupe sur les
runners
LE TEST PORTE SON PROPRE CONTROLE NEGATIF
scripts/tests/test_ecriture_atomique.py rejoue D ABORD l ancienne forme et
verifie qu elle DETRUIT. Sans ce controle, « le fichier est intact » ne
prouverait rien — il pourrait l etre parce que rien n a ete ecrit du tout. Une
garantie qu on n a jamais vue echouer n est pas une garantie, c est une
habitude.
Branche sur P02, dont le titre annoncait « inventory_host » alors qu il lance
maintenant trois tests. Corrige au passage.
LA VOUTE DU GUI : VERIFIEE, PAS DE DEFAUT
Le soupcon etait qu executer_flux pose ANSIBLE_VAULT_PASSWORD_FILE (un seul mot
de passe) alors que creer une VM ouvre DEUX voutes depuis « une voute, une cle ».
Eprouve contre deux voutes JETABLES a mots de passe distincts — jamais les
vraies. Les deux variables se CUMULENT : Ansible essaie tous les secrets, et un
PASSWORD_FILE errone n empeche rien. Confirme en sondant l environnement qu une
recette make recoit reellement : le mot de passe saisi ET les cinq cles
calculees par voutes.py.
Ce qui sauve ce chemin n est donc pas le mot de passe saisi, c est
l IDENTITY_LIST que make pose par-dessus. Chacun couvre ce que l autre ne
couvre pas — le PASSWORD_FILE sert le runner qui n a que sa cle, l IDENTITY_LIST
le poste qui les a toutes. Ecrit au-dessus du code, pour que personne ne
« simplifie » en retirant l un des deux.
make prouver : CONFORME, 59 OK, 0 echec, 1 saute.
make instancier : DIFF VIDE, quatre registres relus, droits 664 preserves.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 14:57:57 -04:00
|
|
|
with ecriture_atomique(path) as inventory_file:
|
2026-06-24 20:17:46 -04:00
|
|
|
yaml.safe_dump(
|
|
|
|
|
data,
|
|
|
|
|
inventory_file,
|
|
|
|
|
default_flow_style=False,
|
|
|
|
|
sort_keys=False,
|
|
|
|
|
allow_unicode=True,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def enfants(data: dict) -> dict:
|
|
|
|
|
return data.setdefault("all", {}).setdefault("children", {})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def assurer_groupe(data: dict, group: str) -> dict:
|
|
|
|
|
group_data = enfants(data).setdefault(group, {})
|
|
|
|
|
group_data.setdefault("hosts", {})
|
|
|
|
|
return group_data
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def variables_hote(data: dict, host: str) -> dict:
|
|
|
|
|
merged: dict = {}
|
|
|
|
|
for group_data in enfants(data).values():
|
|
|
|
|
hosts = group_data.get("hosts", {})
|
|
|
|
|
if host in hosts and isinstance(hosts[host], dict):
|
|
|
|
|
merged.update(hosts[host])
|
|
|
|
|
return merged
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def tous_les_hotes(data: dict) -> list[str]:
|
|
|
|
|
hosts: set[str] = set()
|
|
|
|
|
for group_data in enfants(data).values():
|
|
|
|
|
hosts.update(group_data.get("hosts", {}))
|
|
|
|
|
return sorted(hosts)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def groupes_hote(data: dict, host: str) -> list[str]:
|
|
|
|
|
groups: list[str] = []
|
|
|
|
|
for group, group_data in enfants(data).items():
|
|
|
|
|
if host in group_data.get("hosts", {}):
|
|
|
|
|
groups.append(group)
|
|
|
|
|
return groups
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def retirer_hote_de_tous_les_groupes(data: dict, host: str) -> None:
|
|
|
|
|
for group_data in enfants(data).values():
|
|
|
|
|
group_data.get("hosts", {}).pop(host, None)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def ajouter_hote(
|
|
|
|
|
data: dict,
|
|
|
|
|
host: str,
|
|
|
|
|
groups: list[str],
|
|
|
|
|
ip: str | None,
|
|
|
|
|
ansible_user: str | None,
|
|
|
|
|
vmid: str | None,
|
|
|
|
|
) -> None:
|
|
|
|
|
vars_for_host = variables_hote(data, host)
|
|
|
|
|
if ip:
|
|
|
|
|
vars_for_host["ansible_host"] = ip
|
|
|
|
|
if ansible_user:
|
|
|
|
|
vars_for_host["ansible_user"] = ansible_user
|
|
|
|
|
if vmid:
|
|
|
|
|
vars_for_host["proxmox_vmid"] = int(vmid) if vmid.isdigit() else vmid
|
|
|
|
|
|
|
|
|
|
for group in groups:
|
|
|
|
|
assurer_groupe(data, group)["hosts"][host] = dict(vars_for_host)
|
|
|
|
|
|
|
|
|
|
if ip:
|
|
|
|
|
assurer_groupe(data, GROUPE_HOTES_ACTIFS)["hosts"][host] = dict(vars_for_host)
|
|
|
|
|
assurer_groupe(data, GROUPE_HOTES_PLANIFIES)["hosts"].pop(host, None)
|
|
|
|
|
else:
|
|
|
|
|
assurer_groupe(data, GROUPE_HOTES_PLANIFIES)["hosts"][host] = dict(vars_for_host)
|
|
|
|
|
assurer_groupe(data, GROUPE_HOTES_ACTIFS)["hosts"].pop(host, None)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def definir_groupes_hote(data: dict, host: str, groups: list[str]) -> None:
|
|
|
|
|
vars_for_host = variables_hote(data, host)
|
|
|
|
|
current_groups = groupes_hote(data, host)
|
|
|
|
|
if not current_groups:
|
|
|
|
|
raise ValueError(f"Hote absent de l'inventaire: {host}")
|
|
|
|
|
retirer_hote_de_tous_les_groupes(data, host)
|
|
|
|
|
for group in current_groups:
|
|
|
|
|
if group in GROUPES_ETAT_HOTE:
|
|
|
|
|
assurer_groupe(data, group)["hosts"][host] = dict(vars_for_host)
|
|
|
|
|
for group in groups:
|
|
|
|
|
assurer_groupe(data, group)["hosts"][host] = dict(vars_for_host)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def analyser_groupes(raw_groups: str) -> list[str]:
|
|
|
|
|
groups = [group.strip() for group in raw_groups.replace(",", " ").split() if group.strip()]
|
|
|
|
|
if not groups:
|
|
|
|
|
raise ValueError("Au moins un groupe est requis.")
|
|
|
|
|
return groups
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def afficher_hote(data: dict, host: str) -> None:
|
|
|
|
|
groups = groupes_hote(data, host)
|
|
|
|
|
if not groups:
|
|
|
|
|
raise ValueError(f"Hote absent de l'inventaire: {host}")
|
|
|
|
|
|
|
|
|
|
yaml.safe_dump(
|
|
|
|
|
{
|
|
|
|
|
"hote": host,
|
|
|
|
|
"groupes": groups,
|
|
|
|
|
"variables": variables_hote(data, host),
|
|
|
|
|
},
|
|
|
|
|
sys.stdout,
|
|
|
|
|
default_flow_style=False,
|
|
|
|
|
sort_keys=False,
|
|
|
|
|
allow_unicode=True,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
réseau : le VNet d'une VM se dérive, un hyperviseur a plusieurs pattes (D-55 à D-59)
Le pont n'était pas seulement non portable, il était faux. proxmox_clone_pont
faisait naître les VM sur vmbr1 avec une étiquette VLAN — l'ancien monde. En SDN
une VM appartient à son VNet ; c'est ce qu'il a fallu corriger à la main sur
infra-pki-01, et les treize suivantes auraient suivi.
deriver_nomenclature() expose désormais la zone de sécurité, instancier en dérive
proxmox_pont et une étiquette VIDE — le VNet porte déjà le tag, en poser un
second donnerait un double étiquetage. La chaîne va jusqu'à make creer-vm :
SETOPS_PONT='t11appl', SETOPS_VLAN=''.
Trois pièges. Un doublon dans le Makefile passait PONT_PROXMOX deux fois dans la
même cible, la seconde vide aurait écrasé la valeur dérivée. Un repli naïf sur
proxmox_vlan aurait fait revenir l'étiquette en SDN : le repli ne s'applique que
si la clé est ABSENTE, jamais si elle est présente et vide. Et le test unitaire
est tombé, à raison — il couvre maintenant cette distinction.
D-55 : le dépôt réseau porte le contrat entre l'Alliance et ses hébergeurs, et
abstrait le matériel en encapsulant chaque tenant dans sa zone EVPN. Mesuré : un
tenant est à deux valeurs de la portabilité complète (noeud, stockage).
D-57 : l'interface sysadmin d'un hyperviseur (vmbr0, 10.0.0.41/.43/.47) n'a pas
de route par défaut ; celle-ci vit sur vlan40, vers la frontière. On n'atteint
l'administration que depuis son propre domaine de diffusion. Ça tranche la
question de la sortie des nœuds laissée ouverte ce matin — option A, mais sur une
interface dédiée, ce qui lève l'objection qui la bloquait.
D-58 : un hôte déclare par quelle interface (`via`) chaque réseau lui arrive ; le
devis en dérive un port par interface et son type — trunk 11,40 sur bond3, accès
VLAN 10 sur vmbr0. Sans ça, ajouter le VLAN 10 le remettait sur le trunk du
transport, soit le domaine qu'on venait d'en sortir.
D-59 : un VLAN qui ne porte que des adresses d'hôte n'a pas besoin de pont.
Régression créée puis corrigée : le modèle public, qui ne déclare aucun
hyperviseur, n'émettait plus rien pour ce port. Il émet maintenant tout
l'underlay en disant que c'est un repli.
30 preuves OK, 4 tests unitaires.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 16:20:56 -04:00
|
|
|
CHAMPS_PROXMOX: tuple[tuple, ...] = (
|
2026-06-24 20:17:46 -04:00
|
|
|
("SETOPS_VMID", "proxmox_vmid", True),
|
|
|
|
|
("SETOPS_IP", "ansible_host", True),
|
|
|
|
|
("SETOPS_CIDR", "proxmox_cidr", True),
|
|
|
|
|
("SETOPS_PASSERELLE", "proxmox_passerelle", True),
|
réseau : le VNet d'une VM se dérive, un hyperviseur a plusieurs pattes (D-55 à D-59)
Le pont n'était pas seulement non portable, il était faux. proxmox_clone_pont
faisait naître les VM sur vmbr1 avec une étiquette VLAN — l'ancien monde. En SDN
une VM appartient à son VNet ; c'est ce qu'il a fallu corriger à la main sur
infra-pki-01, et les treize suivantes auraient suivi.
deriver_nomenclature() expose désormais la zone de sécurité, instancier en dérive
proxmox_pont et une étiquette VIDE — le VNet porte déjà le tag, en poser un
second donnerait un double étiquetage. La chaîne va jusqu'à make creer-vm :
SETOPS_PONT='t11appl', SETOPS_VLAN=''.
Trois pièges. Un doublon dans le Makefile passait PONT_PROXMOX deux fois dans la
même cible, la seconde vide aurait écrasé la valeur dérivée. Un repli naïf sur
proxmox_vlan aurait fait revenir l'étiquette en SDN : le repli ne s'applique que
si la clé est ABSENTE, jamais si elle est présente et vide. Et le test unitaire
est tombé, à raison — il couvre maintenant cette distinction.
D-55 : le dépôt réseau porte le contrat entre l'Alliance et ses hébergeurs, et
abstrait le matériel en encapsulant chaque tenant dans sa zone EVPN. Mesuré : un
tenant est à deux valeurs de la portabilité complète (noeud, stockage).
D-57 : l'interface sysadmin d'un hyperviseur (vmbr0, 10.0.0.41/.43/.47) n'a pas
de route par défaut ; celle-ci vit sur vlan40, vers la frontière. On n'atteint
l'administration que depuis son propre domaine de diffusion. Ça tranche la
question de la sortie des nœuds laissée ouverte ce matin — option A, mais sur une
interface dédiée, ce qui lève l'objection qui la bloquait.
D-58 : un hôte déclare par quelle interface (`via`) chaque réseau lui arrive ; le
devis en dérive un port par interface et son type — trunk 11,40 sur bond3, accès
VLAN 10 sur vmbr0. Sans ça, ajouter le VLAN 10 le remettait sur le trunk du
transport, soit le domaine qu'on venait d'en sortir.
D-59 : un VLAN qui ne porte que des adresses d'hôte n'a pas besoin de pont.
Régression créée puis corrigée : le modèle public, qui ne déclare aucun
hyperviseur, n'émettait plus rien pour ce port. Il émet maintenant tout
l'underlay en disant que c'est un repli.
30 preuves OK, 4 tests unitaires.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 16:20:56 -04:00
|
|
|
# L'etiquette VLAN a poser sur la carte. En SDN elle est VIDE : le VNet la porte
|
|
|
|
|
# deja, et en poser une seconde donnerait un double etiquetage. `proxmox_vlan`
|
|
|
|
|
# sert de repli pour un inventaire pas encore regenere — mais SEULEMENT si la cle
|
|
|
|
|
# est ABSENTE, jamais si elle est presente et vide : c'est la difference entre
|
|
|
|
|
# « on ne sait pas » et « on a decide qu'il n'y en a pas ».
|
|
|
|
|
("SETOPS_VLAN", "proxmox_etiquette_vlan", False, "proxmox_vlan"),
|
|
|
|
|
("SETOPS_PONT", "proxmox_pont", False),
|
2026-06-24 20:17:46 -04:00
|
|
|
("SETOPS_STOCKAGE", "proxmox_stockage", False),
|
|
|
|
|
("SETOPS_DISQUE", "proxmox_disque_taille", False),
|
|
|
|
|
("SETOPS_NOEUD", "proxmox_noeud", False),
|
2026-06-30 10:07:04 -04:00
|
|
|
("SETOPS_COEURS", "proxmox_coeurs", False),
|
|
|
|
|
("SETOPS_MEMOIRE", "proxmox_memoire", False),
|
2026-08-24 16:59:34 -04:00
|
|
|
# Resolveur d'AMORCAGE, pose par cloud-init. Il ne sert qu'une fois : `client_resolveur`
|
dns : client_unbound universel, et un resolveur d'amorcage
Une VM ne peut pas s'installer sans resoudre des noms. PowerDNS repond
UNIQUEMENT pour la zone souveraine et ne recurse pour personne : il manquait un
resolveur recursif. `client_unbound` est l'outil ecrit pour ca — il rejoint
`client_journal` et `client_metrique` parmi les integrations universelles.
`infra-dns-01` est exempte (PowerDNS occupe son port 53), et
`serveur_powerdns_listen_addresses` passe de 0.0.0.0 a l'adresse de l'hote pour
laisser 127.0.0.1:53 libre. Mais l'appartenance au groupe est AUSSI ce qui ouvre
le port 53 a la frontiere : en exemptant la machine, je lui retirais le droit de
resoudre. `serveur_powerdns` declare donc son propre flux sortant — il ne
recurse pour personne, mais doit resoudre pour lui-meme.
Nouvel intrant `dns_amorcage`, derive jusqu'a `make creer-vm`. Cloud-init
l'ecrit bien mais sans effet : `dns-nameservers` exige `resolvconf`, absent du
gabarit, et installer resolvconf demande apt, qui demande la resolution.
`serveur_debian` pose donc le resolveur en pre_tasks, avant le premier apt, avec
une garde qui respecte la bascule ulterieure de client_unbound.
Defaut corrige en chemin : `_intrants_communs()` lisait `instance/` en dur ; le
chemin derive maintenant de l'inventaire recu, et un test le prouve.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 21:41:58 -04:00
|
|
|
# bascule ensuite `/etc/resolv.conf` vers 127.0.0.1. Mais sans lui, la VM nait sans
|
|
|
|
|
# resolution de noms et `apt` ne peut meme pas installer Unbound — l'amorcage
|
|
|
|
|
# n'aboutit jamais. Le DNS autoritatif du tenant ne convient PAS : il refuse tout ce
|
|
|
|
|
# qui sort de la zone souveraine.
|
|
|
|
|
("SETOPS_DNS", "dns_amorcage", False),
|
gabarit : deriver le domaine de recherche, et vider resolv.conf a la capture
Question « on peut l'optimiser ? » — mesure avant de repondre. Rien a gagner
cote performance (UEFI/q35, virtio-scsi-single + iothread, discard+ssd,
x86-64-v2-AES, agent, balloon 0) ni cote paquets : le socle est deja cuit dans
l'image.
Ce que le gabarit transporte, c'est son lieu de naissance. searchdomain
chezlepro.ca — le domaine PUBLIC — etait herite par les 14 VM, faute de
proxmox_clone_domaines_recherche defini. Desormais derive de domaine_interne
via SETOPS_DOMAINE, par le mecanisme qui existait deja pour le DNS.
Honnetement : ca ne reparait pas de panne. serveur_debian reecrit resolv.conf
au deploiement sans ligne search — verifie sur la flotte. C'etait faux et ca
ne tenait que par chance.
template_cleanup vide desormais /etc/resolv.conf a la capture : un gabarit ne
transporte aucune identite de reseau.
Notes : mtu 9000 est un reglage MORT (les clones tournent en 1500) ; et le
groupe modeles_vm est VIDE, donc preparer/verifier/nettoyer-modele n'ont
aucune cible.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 10:25:03 -04:00
|
|
|
# Domaine de RECHERCHE de cloud-init. Sans lui, le clone herite de celui du gabarit —
|
|
|
|
|
# `chezlepro.ca`, le domaine PUBLIC du reseau ou l'image a ete fabriquee — alors que
|
|
|
|
|
# les machines vivent dans `chezlepro.internal`. Mesure du 2026-08-09 sur les 14 VM.
|
|
|
|
|
# Sans consequence tant que `/etc/hosts` couvre les noms courts et que la zone
|
|
|
|
|
# publique n'a pas de joker ; les deux tiennent aujourd'hui, aucun des deux n'est
|
|
|
|
|
# garanti. Un nom court resolu vers une adresse publique serait silencieux et faux.
|
|
|
|
|
("SETOPS_DOMAINE", "domaine_interne", False),
|
2026-06-24 20:17:46 -04:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
dns : client_unbound universel, et un resolveur d'amorcage
Une VM ne peut pas s'installer sans resoudre des noms. PowerDNS repond
UNIQUEMENT pour la zone souveraine et ne recurse pour personne : il manquait un
resolveur recursif. `client_unbound` est l'outil ecrit pour ca — il rejoint
`client_journal` et `client_metrique` parmi les integrations universelles.
`infra-dns-01` est exempte (PowerDNS occupe son port 53), et
`serveur_powerdns_listen_addresses` passe de 0.0.0.0 a l'adresse de l'hote pour
laisser 127.0.0.1:53 libre. Mais l'appartenance au groupe est AUSSI ce qui ouvre
le port 53 a la frontiere : en exemptant la machine, je lui retirais le droit de
resoudre. `serveur_powerdns` declare donc son propre flux sortant — il ne
recurse pour personne, mais doit resoudre pour lui-meme.
Nouvel intrant `dns_amorcage`, derive jusqu'a `make creer-vm`. Cloud-init
l'ecrit bien mais sans effet : `dns-nameservers` exige `resolvconf`, absent du
gabarit, et installer resolvconf demande apt, qui demande la resolution.
`serveur_debian` pose donc le resolveur en pre_tasks, avant le premier apt, avec
une garde qui respecte la bascule ulterieure de client_unbound.
Defaut corrige en chemin : `_intrants_communs()` lisait `instance/` en dur ; le
chemin derive maintenant de l'inventaire recu, et un test le prouve.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 21:41:58 -04:00
|
|
|
def _intrants_communs(inventaire: Path | None) -> dict:
|
|
|
|
|
"""Intrants de `group_vars/all/` voisins de CET inventaire, visibles au CLONAGE.
|
|
|
|
|
|
|
|
|
|
L'inventaire genere ne porte que des variables d'HOTE ; un intrant commun comme
|
|
|
|
|
`dns_amorcage` y est donc invisible. Le clonage tourne sur localhost, avant qu'Ansible
|
|
|
|
|
ne resolve les group_vars de la cible : sans cette lecture, le parametre serait lu par
|
|
|
|
|
les roles mais jamais par la machine qui cree la VM.
|
|
|
|
|
|
|
|
|
|
Le chemin DERIVE de l'inventaire recu, jamais code en dur : lire `instance/` pendant
|
|
|
|
|
qu'on raisonne sur un autre inventaire ferait dependre le resultat de l'environnement
|
|
|
|
|
— et un test sur inventaire synthetique irait chercher les intrants de la production.
|
|
|
|
|
"""
|
|
|
|
|
if inventaire is None:
|
|
|
|
|
return {}
|
|
|
|
|
dossier = Path(inventaire).resolve().parent / "group_vars" / "all"
|
|
|
|
|
if not dossier.is_dir():
|
|
|
|
|
return {}
|
|
|
|
|
fusion: dict = {}
|
|
|
|
|
for f in sorted(dossier.glob("*.yml")):
|
|
|
|
|
try:
|
|
|
|
|
contenu = yaml.safe_load(f.read_text(encoding="utf-8"))
|
|
|
|
|
except (OSError, yaml.YAMLError):
|
|
|
|
|
continue # voute chiffree ou fichier illisible : sans objet ici
|
|
|
|
|
if isinstance(contenu, dict):
|
|
|
|
|
fusion.update(contenu)
|
|
|
|
|
return fusion
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def parametres_proxmox_hote(data: dict, host: str,
|
|
|
|
|
inventaire: Path | None = None) -> list[str]:
|
2026-06-24 20:17:46 -04:00
|
|
|
"""Emet les parametres de clonage Proxmox d'un hote, lus depuis l'inventaire.
|
|
|
|
|
|
|
|
|
|
Sortie shell-evaluable (`SETOPS_*='valeur'`) consommee par `make creer-vm`.
|
|
|
|
|
L'inventaire genere est la seule source : aucun parametre n'est saisi a la main.
|
|
|
|
|
"""
|
|
|
|
|
if not groupes_hote(data, host):
|
|
|
|
|
raise ValueError(
|
|
|
|
|
f"Hote absent de l'inventaire: {host}. "
|
|
|
|
|
"Declare-le dans le plan (instance/plan/serveurs.yml) puis "
|
|
|
|
|
"`make instancier-appliquer`."
|
|
|
|
|
)
|
|
|
|
|
|
dns : client_unbound universel, et un resolveur d'amorcage
Une VM ne peut pas s'installer sans resoudre des noms. PowerDNS repond
UNIQUEMENT pour la zone souveraine et ne recurse pour personne : il manquait un
resolveur recursif. `client_unbound` est l'outil ecrit pour ca — il rejoint
`client_journal` et `client_metrique` parmi les integrations universelles.
`infra-dns-01` est exempte (PowerDNS occupe son port 53), et
`serveur_powerdns_listen_addresses` passe de 0.0.0.0 a l'adresse de l'hote pour
laisser 127.0.0.1:53 libre. Mais l'appartenance au groupe est AUSSI ce qui ouvre
le port 53 a la frontiere : en exemptant la machine, je lui retirais le droit de
resoudre. `serveur_powerdns` declare donc son propre flux sortant — il ne
recurse pour personne, mais doit resoudre pour lui-meme.
Nouvel intrant `dns_amorcage`, derive jusqu'a `make creer-vm`. Cloud-init
l'ecrit bien mais sans effet : `dns-nameservers` exige `resolvconf`, absent du
gabarit, et installer resolvconf demande apt, qui demande la resolution.
`serveur_debian` pose donc le resolveur en pre_tasks, avant le premier apt, avec
une garde qui respecte la bascule ulterieure de client_unbound.
Defaut corrige en chemin : `_intrants_communs()` lisait `instance/` en dur ; le
chemin derive maintenant de l'inventaire recu, et un test le prouve.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 21:41:58 -04:00
|
|
|
vars_for_host = _intrants_communs(inventaire) | variables_hote(data, host)
|
2026-06-24 20:17:46 -04:00
|
|
|
lignes: list[str] = []
|
|
|
|
|
manquants: list[str] = []
|
|
|
|
|
|
réseau : le VNet d'une VM se dérive, un hyperviseur a plusieurs pattes (D-55 à D-59)
Le pont n'était pas seulement non portable, il était faux. proxmox_clone_pont
faisait naître les VM sur vmbr1 avec une étiquette VLAN — l'ancien monde. En SDN
une VM appartient à son VNet ; c'est ce qu'il a fallu corriger à la main sur
infra-pki-01, et les treize suivantes auraient suivi.
deriver_nomenclature() expose désormais la zone de sécurité, instancier en dérive
proxmox_pont et une étiquette VIDE — le VNet porte déjà le tag, en poser un
second donnerait un double étiquetage. La chaîne va jusqu'à make creer-vm :
SETOPS_PONT='t11appl', SETOPS_VLAN=''.
Trois pièges. Un doublon dans le Makefile passait PONT_PROXMOX deux fois dans la
même cible, la seconde vide aurait écrasé la valeur dérivée. Un repli naïf sur
proxmox_vlan aurait fait revenir l'étiquette en SDN : le repli ne s'applique que
si la clé est ABSENTE, jamais si elle est présente et vide. Et le test unitaire
est tombé, à raison — il couvre maintenant cette distinction.
D-55 : le dépôt réseau porte le contrat entre l'Alliance et ses hébergeurs, et
abstrait le matériel en encapsulant chaque tenant dans sa zone EVPN. Mesuré : un
tenant est à deux valeurs de la portabilité complète (noeud, stockage).
D-57 : l'interface sysadmin d'un hyperviseur (vmbr0, 10.0.0.41/.43/.47) n'a pas
de route par défaut ; celle-ci vit sur vlan40, vers la frontière. On n'atteint
l'administration que depuis son propre domaine de diffusion. Ça tranche la
question de la sortie des nœuds laissée ouverte ce matin — option A, mais sur une
interface dédiée, ce qui lève l'objection qui la bloquait.
D-58 : un hôte déclare par quelle interface (`via`) chaque réseau lui arrive ; le
devis en dérive un port par interface et son type — trunk 11,40 sur bond3, accès
VLAN 10 sur vmbr0. Sans ça, ajouter le VLAN 10 le remettait sur le trunk du
transport, soit le domaine qu'on venait d'en sortir.
D-59 : un VLAN qui ne porte que des adresses d'hôte n'a pas besoin de pont.
Régression créée puis corrigée : le modèle public, qui ne déclare aucun
hyperviseur, n'émettait plus rien pour ce port. Il émet maintenant tout
l'underlay en disant que c'est un repli.
30 preuves OK, 4 tests unitaires.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 16:20:56 -04:00
|
|
|
for champ in CHAMPS_PROXMOX:
|
|
|
|
|
var_shell, cle, requis = champ[0], champ[1], champ[2]
|
|
|
|
|
repli = champ[3] if len(champ) > 3 else None
|
|
|
|
|
if cle in vars_for_host:
|
|
|
|
|
valeur = vars_for_host.get(cle)
|
|
|
|
|
elif repli:
|
|
|
|
|
valeur = vars_for_host.get(repli)
|
|
|
|
|
else:
|
|
|
|
|
valeur = None
|
2026-06-24 20:17:46 -04:00
|
|
|
if valeur in (None, ""):
|
|
|
|
|
if requis:
|
|
|
|
|
manquants.append(cle)
|
|
|
|
|
valeur = ""
|
|
|
|
|
valeur = str(valeur)
|
|
|
|
|
if "'" in valeur:
|
|
|
|
|
raise ValueError(
|
|
|
|
|
f"Valeur Proxmox inattendue pour {cle} ({host}): apostrophe interdite."
|
|
|
|
|
)
|
|
|
|
|
lignes.append(f"{var_shell}='{valeur}'")
|
|
|
|
|
|
|
|
|
|
if manquants:
|
|
|
|
|
raise ValueError(
|
|
|
|
|
f"Parametres Proxmox manquants pour {host}: {', '.join(manquants)}. "
|
|
|
|
|
"Regenere l'inventaire (`make instancier-appliquer`)."
|
|
|
|
|
)
|
|
|
|
|
|
insemination : la cle d'amorcage, bornee au meme groupe que le flux
Le flux etait declare et applique ; il manquait l'IDENTITE. Le runner du SITE
atteignait la porte de ops-01 sans avoir de cle.
LE PIEGE COMPTE PLUS QUE LE CORRECTIF. La cle s'injecte au CLONAGE, et `creer-vm`
cree TOUTES les machines d'un tenant. L'injecter a chaque clonage aurait donne a
l'hebergeur un acces SSH a la flotte entiere de chaque locataire, en silence — ca
aurait defait a la couche IDENTITE ce que le pare-feu venait de borner a la couche
RESEAU. Le meme critere gouverne donc les deux : porter `serveur_ops_tenant`.
`SETOPS_CLES_AMORCAGE` est vide partout ailleurs. Elle S'AJOUTE a celle de
l'exploitant, elle ne la remplace pas : c'est l'humain qui arme.
La cle vient du PLAN DU SITE, pas du disque local : materialiser depuis le poste
et depuis le runner doit produire la meme VM.
DEUX COUCHES MANGEAIENT LES ESPACES. Proxmox rendait `SSH public key validation
error` — message muet sur la cause. Isole par un CONTROLE (rejouer sans la cle :
la tache passe), puis par la mesure de ce qui arrivait au module :
"sshkeys": "ssh-ed25519" <- le premier mot, rien d'autre
J'ai accuse `make` d'abord ; c'etait `ansible-playbook -e cle=valeur`, qui decoupe
AU SHLEX. D'ou l'environnement pour le transport et `-e '{...}'` en JSON pour
l'entree. (Un scalaire YAML plie ne produit pas non plus de saut de ligne.)
TROIS TESTS QUI NE GARDAIENT RIEN. `test_inventory_host` inscrit ses tests dans une
liste explicite ; mes deux nouveaux n'y etaient pas — definis, jamais joues. La
garde d'exhaustivite ajoutee en a trouve un TROISIEME le jour meme,
`test_etiquette_vlan_repli_et_vide_explicite`, jamais inscrit depuis sa creation :
inscrit, il levait un KeyError sur une fixture qu'il lisait mal. Un test non
inscrit est pire qu'un test absent : on croit l'avoir.
PREUVE, AVEC SON CONTROLE NEGATIF :
runner du SITE -> ops-01 ops-01 10.17.19.41/24 entre
runner du SITE -> 10.17.19.21 Connection timed out refuse
make verifier : vert. make prouver : CONFORME, 52 OK, 0 echec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-08-28 13:45:44 -04:00
|
|
|
# LA CLE D'AMORCAGE — POSEE SUR LE RUNNER D'UN TENANT, ET SUR LUI SEUL.
|
|
|
|
|
#
|
|
|
|
|
# Le runner du SITE doit pouvoir entrer chez le runner du tenant pour l'amorcer :
|
|
|
|
|
# c'est l'insemination, et son flux est declare dans
|
|
|
|
|
# `roles/serveur_ops_site/meta/flux.yml`. Il lui faut donc une identite sur cette
|
|
|
|
|
# machine — une cle publique, qui n'ouvre rien par elle-meme.
|
|
|
|
|
#
|
|
|
|
|
# LE CRITERE EST LE MEME QUE CELUI DU FLUX : porter `serveur_ops_tenant`. C'est ce
|
|
|
|
|
# qui empeche le geste de deraper. `creer-vm` cree TOUTES les machines d'un tenant ;
|
|
|
|
|
# injecter cette cle a chaque clonage donnerait au site un acces SSH a la flotte
|
|
|
|
|
# entiere de chaque locataire, sans qu'aucune regle ne le dise — et ca deferait a la
|
|
|
|
|
# couche IDENTITE ce que le pare-feu borne a la couche RESEAU. Deux couches qui ne
|
|
|
|
|
# declarent pas la meme politique, c'est une politique qu'on ne peut plus lire.
|
|
|
|
|
#
|
|
|
|
|
# La cle vient du PLAN DU SITE, pas du disque local : on materialise depuis le runner
|
|
|
|
|
# comme depuis le poste de l'exploitant, et un `lookup` local rendrait deux valeurs
|
|
|
|
|
# differentes selon qui agit — la VM nee du poste n'accepterait pas le runner.
|
cles : le terrain etait inoccupable — la cle du tenant nait avec ses machines
Le deploiement lance depuis ops-01 s est arrete au premier geste, sur les quinze
machines a la fois : Permission denied (publickey). Les VM neuves n acceptaient que
la cle de l exploitant. Celle du runner EST declaree au plan, mais c est le SOCLE
qui la depose — et le socle doit etre applique par quelqu un qui peut deja entrer.
Boucle fermee : le tenant recevait un terrain qu il ne pouvait pas occuper.
DEUX CLES, DEUX PORTEES :
la cle du SITE -> sur le SEUL runner du tenant l insemination
la cle du TENANT -> sur TOUTES ses machines il va les configurer
POSER UNE CLE AU CLONAGE N EST PAS ENTRER CHEZ LE TENANT. C est un parametre de
creation, au meme titre que l adresse ou le disque : le site ecrit les conditions
de NAISSANCE, il n ouvre aucune session. Le site n obtient aucun acces sur ces
machines ; seul le runner du tenant en obtient un.
Forme tranchee par l exploitant : le site renseigne le seul runner, qui se charge
de toute sa flotte. Le plancher /etc/hosts suit le meme chemin — ops-01 a le sien
depuis son insemination et resout ses quinze voisines par leur nom.
LA REVOCATION EST HONOREE A LA NAISSANCE : une entree a etat absent n est pas
reposee. Sans cette lecture, une cle retiree de la flotte serait ressuscitee sur
chaque VM creee ensuite — panne lente, silencieuse, invisible au plan.
P55 garde les deux moities : la cle du site ne nait que sur un porteur de
serveur_ops_tenant, et aucune machine ne reste sans celle de son tenant. Une
frontiere tenue a une seule couche n est pas tenue. Deux controles negatifs.
make verifier : vert. make prouver : CONFORME, 55 OK, 0 echec, 0 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-08-30 08:54:17 -04:00
|
|
|
lignes.append(f"SETOPS_CLES_AMORCAGE='{_cle_amorcage(data, host, inventaire)}'")
|
insemination : la cle d'amorcage, bornee au meme groupe que le flux
Le flux etait declare et applique ; il manquait l'IDENTITE. Le runner du SITE
atteignait la porte de ops-01 sans avoir de cle.
LE PIEGE COMPTE PLUS QUE LE CORRECTIF. La cle s'injecte au CLONAGE, et `creer-vm`
cree TOUTES les machines d'un tenant. L'injecter a chaque clonage aurait donne a
l'hebergeur un acces SSH a la flotte entiere de chaque locataire, en silence — ca
aurait defait a la couche IDENTITE ce que le pare-feu venait de borner a la couche
RESEAU. Le meme critere gouverne donc les deux : porter `serveur_ops_tenant`.
`SETOPS_CLES_AMORCAGE` est vide partout ailleurs. Elle S'AJOUTE a celle de
l'exploitant, elle ne la remplace pas : c'est l'humain qui arme.
La cle vient du PLAN DU SITE, pas du disque local : materialiser depuis le poste
et depuis le runner doit produire la meme VM.
DEUX COUCHES MANGEAIENT LES ESPACES. Proxmox rendait `SSH public key validation
error` — message muet sur la cause. Isole par un CONTROLE (rejouer sans la cle :
la tache passe), puis par la mesure de ce qui arrivait au module :
"sshkeys": "ssh-ed25519" <- le premier mot, rien d'autre
J'ai accuse `make` d'abord ; c'etait `ansible-playbook -e cle=valeur`, qui decoupe
AU SHLEX. D'ou l'environnement pour le transport et `-e '{...}'` en JSON pour
l'entree. (Un scalaire YAML plie ne produit pas non plus de saut de ligne.)
TROIS TESTS QUI NE GARDAIENT RIEN. `test_inventory_host` inscrit ses tests dans une
liste explicite ; mes deux nouveaux n'y etaient pas — definis, jamais joues. La
garde d'exhaustivite ajoutee en a trouve un TROISIEME le jour meme,
`test_etiquette_vlan_repli_et_vide_explicite`, jamais inscrit depuis sa creation :
inscrit, il levait un KeyError sur une fixture qu'il lisait mal. Un test non
inscrit est pire qu'un test absent : on croit l'avoir.
PREUVE, AVEC SON CONTROLE NEGATIF :
runner du SITE -> ops-01 ops-01 10.17.19.41/24 entre
runner du SITE -> 10.17.19.21 Connection timed out refuse
make verifier : vert. make prouver : CONFORME, 52 OK, 0 echec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-08-28 13:45:44 -04:00
|
|
|
|
2026-06-24 20:17:46 -04:00
|
|
|
return lignes
|
|
|
|
|
|
|
|
|
|
|
insemination : la cle d'amorcage, bornee au meme groupe que le flux
Le flux etait declare et applique ; il manquait l'IDENTITE. Le runner du SITE
atteignait la porte de ops-01 sans avoir de cle.
LE PIEGE COMPTE PLUS QUE LE CORRECTIF. La cle s'injecte au CLONAGE, et `creer-vm`
cree TOUTES les machines d'un tenant. L'injecter a chaque clonage aurait donne a
l'hebergeur un acces SSH a la flotte entiere de chaque locataire, en silence — ca
aurait defait a la couche IDENTITE ce que le pare-feu venait de borner a la couche
RESEAU. Le meme critere gouverne donc les deux : porter `serveur_ops_tenant`.
`SETOPS_CLES_AMORCAGE` est vide partout ailleurs. Elle S'AJOUTE a celle de
l'exploitant, elle ne la remplace pas : c'est l'humain qui arme.
La cle vient du PLAN DU SITE, pas du disque local : materialiser depuis le poste
et depuis le runner doit produire la meme VM.
DEUX COUCHES MANGEAIENT LES ESPACES. Proxmox rendait `SSH public key validation
error` — message muet sur la cause. Isole par un CONTROLE (rejouer sans la cle :
la tache passe), puis par la mesure de ce qui arrivait au module :
"sshkeys": "ssh-ed25519" <- le premier mot, rien d'autre
J'ai accuse `make` d'abord ; c'etait `ansible-playbook -e cle=valeur`, qui decoupe
AU SHLEX. D'ou l'environnement pour le transport et `-e '{...}'` en JSON pour
l'entree. (Un scalaire YAML plie ne produit pas non plus de saut de ligne.)
TROIS TESTS QUI NE GARDAIENT RIEN. `test_inventory_host` inscrit ses tests dans une
liste explicite ; mes deux nouveaux n'y etaient pas — definis, jamais joues. La
garde d'exhaustivite ajoutee en a trouve un TROISIEME le jour meme,
`test_etiquette_vlan_repli_et_vide_explicite`, jamais inscrit depuis sa creation :
inscrit, il levait un KeyError sur une fixture qu'il lisait mal. Un test non
inscrit est pire qu'un test absent : on croit l'avoir.
PREUVE, AVEC SON CONTROLE NEGATIF :
runner du SITE -> ops-01 ops-01 10.17.19.41/24 entre
runner du SITE -> 10.17.19.21 Connection timed out refuse
make verifier : vert. make prouver : CONFORME, 52 OK, 0 echec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-08-28 13:45:44 -04:00
|
|
|
GROUPE_RUNNER_TENANT = "serveur_ops_tenant"
|
|
|
|
|
|
|
|
|
|
|
cles : le terrain etait inoccupable — la cle du tenant nait avec ses machines
Le deploiement lance depuis ops-01 s est arrete au premier geste, sur les quinze
machines a la fois : Permission denied (publickey). Les VM neuves n acceptaient que
la cle de l exploitant. Celle du runner EST declaree au plan, mais c est le SOCLE
qui la depose — et le socle doit etre applique par quelqu un qui peut deja entrer.
Boucle fermee : le tenant recevait un terrain qu il ne pouvait pas occuper.
DEUX CLES, DEUX PORTEES :
la cle du SITE -> sur le SEUL runner du tenant l insemination
la cle du TENANT -> sur TOUTES ses machines il va les configurer
POSER UNE CLE AU CLONAGE N EST PAS ENTRER CHEZ LE TENANT. C est un parametre de
creation, au meme titre que l adresse ou le disque : le site ecrit les conditions
de NAISSANCE, il n ouvre aucune session. Le site n obtient aucun acces sur ces
machines ; seul le runner du tenant en obtient un.
Forme tranchee par l exploitant : le site renseigne le seul runner, qui se charge
de toute sa flotte. Le plancher /etc/hosts suit le meme chemin — ops-01 a le sien
depuis son insemination et resout ses quinze voisines par leur nom.
LA REVOCATION EST HONOREE A LA NAISSANCE : une entree a etat absent n est pas
reposee. Sans cette lecture, une cle retiree de la flotte serait ressuscitee sur
chaque VM creee ensuite — panne lente, silencieuse, invisible au plan.
P55 garde les deux moities : la cle du site ne nait que sur un porteur de
serveur_ops_tenant, et aucune machine ne reste sans celle de son tenant. Une
frontiere tenue a une seule couche n est pas tenue. Deux controles negatifs.
make verifier : vert. make prouver : CONFORME, 55 OK, 0 echec, 0 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-08-30 08:54:17 -04:00
|
|
|
def _cle_amorcage(data: dict, host: str, inventaire: Path | None = None) -> str:
|
|
|
|
|
"""Les cles publiques a poser sur cet hote A SA NAISSANCE. Une par ligne.
|
insemination : la cle d'amorcage, bornee au meme groupe que le flux
Le flux etait declare et applique ; il manquait l'IDENTITE. Le runner du SITE
atteignait la porte de ops-01 sans avoir de cle.
LE PIEGE COMPTE PLUS QUE LE CORRECTIF. La cle s'injecte au CLONAGE, et `creer-vm`
cree TOUTES les machines d'un tenant. L'injecter a chaque clonage aurait donne a
l'hebergeur un acces SSH a la flotte entiere de chaque locataire, en silence — ca
aurait defait a la couche IDENTITE ce que le pare-feu venait de borner a la couche
RESEAU. Le meme critere gouverne donc les deux : porter `serveur_ops_tenant`.
`SETOPS_CLES_AMORCAGE` est vide partout ailleurs. Elle S'AJOUTE a celle de
l'exploitant, elle ne la remplace pas : c'est l'humain qui arme.
La cle vient du PLAN DU SITE, pas du disque local : materialiser depuis le poste
et depuis le runner doit produire la meme VM.
DEUX COUCHES MANGEAIENT LES ESPACES. Proxmox rendait `SSH public key validation
error` — message muet sur la cause. Isole par un CONTROLE (rejouer sans la cle :
la tache passe), puis par la mesure de ce qui arrivait au module :
"sshkeys": "ssh-ed25519" <- le premier mot, rien d'autre
J'ai accuse `make` d'abord ; c'etait `ansible-playbook -e cle=valeur`, qui decoupe
AU SHLEX. D'ou l'environnement pour le transport et `-e '{...}'` en JSON pour
l'entree. (Un scalaire YAML plie ne produit pas non plus de saut de ligne.)
TROIS TESTS QUI NE GARDAIENT RIEN. `test_inventory_host` inscrit ses tests dans une
liste explicite ; mes deux nouveaux n'y etaient pas — definis, jamais joues. La
garde d'exhaustivite ajoutee en a trouve un TROISIEME le jour meme,
`test_etiquette_vlan_repli_et_vide_explicite`, jamais inscrit depuis sa creation :
inscrit, il levait un KeyError sur une fixture qu'il lisait mal. Un test non
inscrit est pire qu'un test absent : on croit l'avoir.
PREUVE, AVEC SON CONTROLE NEGATIF :
runner du SITE -> ops-01 ops-01 10.17.19.41/24 entre
runner du SITE -> 10.17.19.21 Connection timed out refuse
make verifier : vert. make prouver : CONFORME, 52 OK, 0 echec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-08-28 13:45:44 -04:00
|
|
|
|
cles : le terrain etait inoccupable — la cle du tenant nait avec ses machines
Le deploiement lance depuis ops-01 s est arrete au premier geste, sur les quinze
machines a la fois : Permission denied (publickey). Les VM neuves n acceptaient que
la cle de l exploitant. Celle du runner EST declaree au plan, mais c est le SOCLE
qui la depose — et le socle doit etre applique par quelqu un qui peut deja entrer.
Boucle fermee : le tenant recevait un terrain qu il ne pouvait pas occuper.
DEUX CLES, DEUX PORTEES :
la cle du SITE -> sur le SEUL runner du tenant l insemination
la cle du TENANT -> sur TOUTES ses machines il va les configurer
POSER UNE CLE AU CLONAGE N EST PAS ENTRER CHEZ LE TENANT. C est un parametre de
creation, au meme titre que l adresse ou le disque : le site ecrit les conditions
de NAISSANCE, il n ouvre aucune session. Le site n obtient aucun acces sur ces
machines ; seul le runner du tenant en obtient un.
Forme tranchee par l exploitant : le site renseigne le seul runner, qui se charge
de toute sa flotte. Le plancher /etc/hosts suit le meme chemin — ops-01 a le sien
depuis son insemination et resout ses quinze voisines par leur nom.
LA REVOCATION EST HONOREE A LA NAISSANCE : une entree a etat absent n est pas
reposee. Sans cette lecture, une cle retiree de la flotte serait ressuscitee sur
chaque VM creee ensuite — panne lente, silencieuse, invisible au plan.
P55 garde les deux moities : la cle du site ne nait que sur un porteur de
serveur_ops_tenant, et aucune machine ne reste sans celle de son tenant. Une
frontiere tenue a une seule couche n est pas tenue. Deux controles negatifs.
make verifier : vert. make prouver : CONFORME, 55 OK, 0 echec, 0 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-08-30 08:54:17 -04:00
|
|
|
DEUX CLES, DEUX PORTEES, ET LA DIFFERENCE EST TOUTE L'ARCHITECTURE :
|
|
|
|
|
|
|
|
|
|
la cle du SITE -> sur le SEUL runner du tenant. C'est l'insemination : le site
|
|
|
|
|
amorce une machine, et une seule, puis s'arrete.
|
|
|
|
|
la cle du TENANT -> sur TOUTES les machines de ce tenant. C'est son runner qui
|
|
|
|
|
les configurera, et il ne peut pas le faire sans y entrer.
|
|
|
|
|
|
|
|
|
|
POSER UNE CLE AU CLONAGE N'EST PAS ENTRER CHEZ LE TENANT. C'est un parametre de
|
|
|
|
|
creation, au meme titre que l'adresse ou le disque : le site ecrit les conditions de
|
|
|
|
|
NAISSANCE de la machine, il n'ouvre aucune session dedans. La distinction n'est pas
|
|
|
|
|
rhetorique — le site n'obtient aucun acces sur ces machines, seul le runner du tenant
|
|
|
|
|
en obtient un.
|
|
|
|
|
|
|
|
|
|
SANS ELLE, LE TERRAIN EST INOCCUPABLE (mesure du 2026-08-30). Quinze VM creees, et le
|
|
|
|
|
runner du tenant se voyait refuser l'entree sur les quinze :
|
|
|
|
|
|
|
|
|
|
ops-01 -> toutes : Permission denied (publickey)
|
|
|
|
|
|
|
|
|
|
Sa cle EST declaree au plan (`ssh_baseline_cles_admin`), mais c'est le SOCLE qui la
|
|
|
|
|
depose — et le socle doit etre applique par quelqu'un qui peut deja entrer. Boucle
|
|
|
|
|
fermee : le tenant recevait un terrain qu'il ne pouvait pas occuper.
|
|
|
|
|
|
|
|
|
|
Rend '' pour ce qui n'est pas trouve, jamais une valeur devinee : un ecosysteme dont
|
|
|
|
|
le plan ne declare rien se materialise comme avant, avec la seule cle de l'exploitant.
|
|
|
|
|
"""
|
|
|
|
|
cles: list[str] = []
|
|
|
|
|
|
|
|
|
|
# 1. LA CLE DU SITE — le runner du tenant, et lui seul.
|
|
|
|
|
if GROUPE_RUNNER_TENANT in groupes_hote(data, host):
|
|
|
|
|
try:
|
|
|
|
|
import underlay as underlay_mod
|
|
|
|
|
c = str((underlay_mod.lire_plan_site("10-intrants.yml") or {})
|
|
|
|
|
.get("runner_cle_publique") or "").strip()
|
|
|
|
|
if c:
|
|
|
|
|
cles.append(c)
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
# 2. LA CLE DU TENANT — toutes ses machines, y compris son runner (il se joint
|
|
|
|
|
# lui-meme comme les autres, et l'exclure creerait un cas particulier de plus).
|
|
|
|
|
#
|
|
|
|
|
# ELLE VIENT DU PLAN DU TENANT, pas d'un fichier lu sur une machine : materialiser
|
|
|
|
|
# depuis le poste et depuis le runner du site doit produire la meme VM.
|
|
|
|
|
for entree in _cles_admin_du_plan(inventaire):
|
|
|
|
|
if entree not in cles:
|
|
|
|
|
cles.append(entree)
|
|
|
|
|
|
|
|
|
|
for c in cles:
|
|
|
|
|
if "'" in c:
|
|
|
|
|
raise ValueError(
|
|
|
|
|
f"Une cle publique declaree contient une apostrophe ({host}) : "
|
|
|
|
|
"la valeur passe par une commande shell, elle ne peut pas en porter."
|
|
|
|
|
)
|
|
|
|
|
return "\n".join(cles)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _cles_admin_du_plan(inventaire: Path | None) -> list[str]:
|
|
|
|
|
"""Les cles `ssh_baseline_cles_admin` de l'instance, celles a l'etat `present`.
|
|
|
|
|
|
|
|
|
|
ON HONORE L'ETAT. Une entree passee a `absent` est une REVOCATION : la reposer a la
|
|
|
|
|
naissance ressusciterait sur chaque VM neuve une cle qu'on vient de retirer de toute
|
|
|
|
|
la flotte — la panne serait lente, silencieuse, et parfaitement invisible au plan.
|
insemination : la cle d'amorcage, bornee au meme groupe que le flux
Le flux etait declare et applique ; il manquait l'IDENTITE. Le runner du SITE
atteignait la porte de ops-01 sans avoir de cle.
LE PIEGE COMPTE PLUS QUE LE CORRECTIF. La cle s'injecte au CLONAGE, et `creer-vm`
cree TOUTES les machines d'un tenant. L'injecter a chaque clonage aurait donne a
l'hebergeur un acces SSH a la flotte entiere de chaque locataire, en silence — ca
aurait defait a la couche IDENTITE ce que le pare-feu venait de borner a la couche
RESEAU. Le meme critere gouverne donc les deux : porter `serveur_ops_tenant`.
`SETOPS_CLES_AMORCAGE` est vide partout ailleurs. Elle S'AJOUTE a celle de
l'exploitant, elle ne la remplace pas : c'est l'humain qui arme.
La cle vient du PLAN DU SITE, pas du disque local : materialiser depuis le poste
et depuis le runner doit produire la meme VM.
DEUX COUCHES MANGEAIENT LES ESPACES. Proxmox rendait `SSH public key validation
error` — message muet sur la cause. Isole par un CONTROLE (rejouer sans la cle :
la tache passe), puis par la mesure de ce qui arrivait au module :
"sshkeys": "ssh-ed25519" <- le premier mot, rien d'autre
J'ai accuse `make` d'abord ; c'etait `ansible-playbook -e cle=valeur`, qui decoupe
AU SHLEX. D'ou l'environnement pour le transport et `-e '{...}'` en JSON pour
l'entree. (Un scalaire YAML plie ne produit pas non plus de saut de ligne.)
TROIS TESTS QUI NE GARDAIENT RIEN. `test_inventory_host` inscrit ses tests dans une
liste explicite ; mes deux nouveaux n'y etaient pas — definis, jamais joues. La
garde d'exhaustivite ajoutee en a trouve un TROISIEME le jour meme,
`test_etiquette_vlan_repli_et_vide_explicite`, jamais inscrit depuis sa creation :
inscrit, il levait un KeyError sur une fixture qu'il lisait mal. Un test non
inscrit est pire qu'un test absent : on croit l'avoir.
PREUVE, AVEC SON CONTROLE NEGATIF :
runner du SITE -> ops-01 ops-01 10.17.19.41/24 entre
runner du SITE -> 10.17.19.21 Connection timed out refuse
make verifier : vert. make prouver : CONFORME, 52 OK, 0 echec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-08-28 13:45:44 -04:00
|
|
|
"""
|
|
|
|
|
try:
|
cles : le terrain etait inoccupable — la cle du tenant nait avec ses machines
Le deploiement lance depuis ops-01 s est arrete au premier geste, sur les quinze
machines a la fois : Permission denied (publickey). Les VM neuves n acceptaient que
la cle de l exploitant. Celle du runner EST declaree au plan, mais c est le SOCLE
qui la depose — et le socle doit etre applique par quelqu un qui peut deja entrer.
Boucle fermee : le tenant recevait un terrain qu il ne pouvait pas occuper.
DEUX CLES, DEUX PORTEES :
la cle du SITE -> sur le SEUL runner du tenant l insemination
la cle du TENANT -> sur TOUTES ses machines il va les configurer
POSER UNE CLE AU CLONAGE N EST PAS ENTRER CHEZ LE TENANT. C est un parametre de
creation, au meme titre que l adresse ou le disque : le site ecrit les conditions
de NAISSANCE, il n ouvre aucune session. Le site n obtient aucun acces sur ces
machines ; seul le runner du tenant en obtient un.
Forme tranchee par l exploitant : le site renseigne le seul runner, qui se charge
de toute sa flotte. Le plancher /etc/hosts suit le meme chemin — ops-01 a le sien
depuis son insemination et resout ses quinze voisines par leur nom.
LA REVOCATION EST HONOREE A LA NAISSANCE : une entree a etat absent n est pas
reposee. Sans cette lecture, une cle retiree de la flotte serait ressuscitee sur
chaque VM creee ensuite — panne lente, silencieuse, invisible au plan.
P55 garde les deux moities : la cle du site ne nait que sur un porteur de
serveur_ops_tenant, et aucune machine ne reste sans celle de son tenant. Une
frontiere tenue a une seule couche n est pas tenue. Deux controles negatifs.
make verifier : vert. make prouver : CONFORME, 55 OK, 0 echec, 0 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-08-30 08:54:17 -04:00
|
|
|
depuis = Path(inventaire).parent if inventaire else None
|
|
|
|
|
gv = (depuis or _instance_dir()) / "group_vars" / "all"
|
|
|
|
|
for fichier in sorted(gv.glob("*.yml")):
|
|
|
|
|
if "vault" in fichier.name:
|
|
|
|
|
continue
|
|
|
|
|
data = yaml.safe_load(fichier.read_text(encoding="utf-8")) or {}
|
|
|
|
|
if isinstance(data, dict) and data.get("ssh_baseline_cles_admin"):
|
|
|
|
|
return [str(e.get("cle", "")).strip()
|
|
|
|
|
for e in data["ssh_baseline_cles_admin"]
|
|
|
|
|
if isinstance(e, dict)
|
|
|
|
|
and str(e.get("etat", "present")) == "present"
|
|
|
|
|
and str(e.get("cle", "")).strip()]
|
insemination : la cle d'amorcage, bornee au meme groupe que le flux
Le flux etait declare et applique ; il manquait l'IDENTITE. Le runner du SITE
atteignait la porte de ops-01 sans avoir de cle.
LE PIEGE COMPTE PLUS QUE LE CORRECTIF. La cle s'injecte au CLONAGE, et `creer-vm`
cree TOUTES les machines d'un tenant. L'injecter a chaque clonage aurait donne a
l'hebergeur un acces SSH a la flotte entiere de chaque locataire, en silence — ca
aurait defait a la couche IDENTITE ce que le pare-feu venait de borner a la couche
RESEAU. Le meme critere gouverne donc les deux : porter `serveur_ops_tenant`.
`SETOPS_CLES_AMORCAGE` est vide partout ailleurs. Elle S'AJOUTE a celle de
l'exploitant, elle ne la remplace pas : c'est l'humain qui arme.
La cle vient du PLAN DU SITE, pas du disque local : materialiser depuis le poste
et depuis le runner doit produire la meme VM.
DEUX COUCHES MANGEAIENT LES ESPACES. Proxmox rendait `SSH public key validation
error` — message muet sur la cause. Isole par un CONTROLE (rejouer sans la cle :
la tache passe), puis par la mesure de ce qui arrivait au module :
"sshkeys": "ssh-ed25519" <- le premier mot, rien d'autre
J'ai accuse `make` d'abord ; c'etait `ansible-playbook -e cle=valeur`, qui decoupe
AU SHLEX. D'ou l'environnement pour le transport et `-e '{...}'` en JSON pour
l'entree. (Un scalaire YAML plie ne produit pas non plus de saut de ligne.)
TROIS TESTS QUI NE GARDAIENT RIEN. `test_inventory_host` inscrit ses tests dans une
liste explicite ; mes deux nouveaux n'y etaient pas — definis, jamais joues. La
garde d'exhaustivite ajoutee en a trouve un TROISIEME le jour meme,
`test_etiquette_vlan_repli_et_vide_explicite`, jamais inscrit depuis sa creation :
inscrit, il levait un KeyError sur une fixture qu'il lisait mal. Un test non
inscrit est pire qu'un test absent : on croit l'avoir.
PREUVE, AVEC SON CONTROLE NEGATIF :
runner du SITE -> ops-01 ops-01 10.17.19.41/24 entre
runner du SITE -> 10.17.19.21 Connection timed out refuse
make verifier : vert. make prouver : CONFORME, 52 OK, 0 echec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-08-28 13:45:44 -04:00
|
|
|
except Exception:
|
cles : le terrain etait inoccupable — la cle du tenant nait avec ses machines
Le deploiement lance depuis ops-01 s est arrete au premier geste, sur les quinze
machines a la fois : Permission denied (publickey). Les VM neuves n acceptaient que
la cle de l exploitant. Celle du runner EST declaree au plan, mais c est le SOCLE
qui la depose — et le socle doit etre applique par quelqu un qui peut deja entrer.
Boucle fermee : le tenant recevait un terrain qu il ne pouvait pas occuper.
DEUX CLES, DEUX PORTEES :
la cle du SITE -> sur le SEUL runner du tenant l insemination
la cle du TENANT -> sur TOUTES ses machines il va les configurer
POSER UNE CLE AU CLONAGE N EST PAS ENTRER CHEZ LE TENANT. C est un parametre de
creation, au meme titre que l adresse ou le disque : le site ecrit les conditions
de NAISSANCE, il n ouvre aucune session. Le site n obtient aucun acces sur ces
machines ; seul le runner du tenant en obtient un.
Forme tranchee par l exploitant : le site renseigne le seul runner, qui se charge
de toute sa flotte. Le plancher /etc/hosts suit le meme chemin — ops-01 a le sien
depuis son insemination et resout ses quinze voisines par leur nom.
LA REVOCATION EST HONOREE A LA NAISSANCE : une entree a etat absent n est pas
reposee. Sans cette lecture, une cle retiree de la flotte serait ressuscitee sur
chaque VM creee ensuite — panne lente, silencieuse, invisible au plan.
P55 garde les deux moities : la cle du site ne nait que sur un porteur de
serveur_ops_tenant, et aucune machine ne reste sans celle de son tenant. Une
frontiere tenue a une seule couche n est pas tenue. Deux controles negatifs.
make verifier : vert. make prouver : CONFORME, 55 OK, 0 echec, 0 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-08-30 08:54:17 -04:00
|
|
|
pass
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _instance_dir() -> Path:
|
|
|
|
|
from inventory_rules import instance_courante
|
|
|
|
|
return Path(instance_courante())
|
insemination : la cle d'amorcage, bornee au meme groupe que le flux
Le flux etait declare et applique ; il manquait l'IDENTITE. Le runner du SITE
atteignait la porte de ops-01 sans avoir de cle.
LE PIEGE COMPTE PLUS QUE LE CORRECTIF. La cle s'injecte au CLONAGE, et `creer-vm`
cree TOUTES les machines d'un tenant. L'injecter a chaque clonage aurait donne a
l'hebergeur un acces SSH a la flotte entiere de chaque locataire, en silence — ca
aurait defait a la couche IDENTITE ce que le pare-feu venait de borner a la couche
RESEAU. Le meme critere gouverne donc les deux : porter `serveur_ops_tenant`.
`SETOPS_CLES_AMORCAGE` est vide partout ailleurs. Elle S'AJOUTE a celle de
l'exploitant, elle ne la remplace pas : c'est l'humain qui arme.
La cle vient du PLAN DU SITE, pas du disque local : materialiser depuis le poste
et depuis le runner doit produire la meme VM.
DEUX COUCHES MANGEAIENT LES ESPACES. Proxmox rendait `SSH public key validation
error` — message muet sur la cause. Isole par un CONTROLE (rejouer sans la cle :
la tache passe), puis par la mesure de ce qui arrivait au module :
"sshkeys": "ssh-ed25519" <- le premier mot, rien d'autre
J'ai accuse `make` d'abord ; c'etait `ansible-playbook -e cle=valeur`, qui decoupe
AU SHLEX. D'ou l'environnement pour le transport et `-e '{...}'` en JSON pour
l'entree. (Un scalaire YAML plie ne produit pas non plus de saut de ligne.)
TROIS TESTS QUI NE GARDAIENT RIEN. `test_inventory_host` inscrit ses tests dans une
liste explicite ; mes deux nouveaux n'y etaient pas — definis, jamais joues. La
garde d'exhaustivite ajoutee en a trouve un TROISIEME le jour meme,
`test_etiquette_vlan_repli_et_vide_explicite`, jamais inscrit depuis sa creation :
inscrit, il levait un KeyError sur une fixture qu'il lisait mal. Un test non
inscrit est pire qu'un test absent : on croit l'avoir.
PREUVE, AVEC SON CONTROLE NEGATIF :
runner du SITE -> ops-01 ops-01 10.17.19.41/24 entre
runner du SITE -> 10.17.19.21 Connection timed out refuse
make verifier : vert. make prouver : CONFORME, 52 OK, 0 echec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-08-28 13:45:44 -04:00
|
|
|
|
|
|
|
|
|
deployer : l'ordre des couches, et l'autorite qui se signe elle-meme
`make deployer` triait les groupes alphabetiquement apres le socle :
`client_metrique` passait avant `serveur_step_ca`, donc exigeait un certificat
que l'autorite, pas encore deployee, ne pouvait pas avoir emis — sur l'hote de
l'AC lui-meme. `docs/couches-deploiement.yml` existe pour definir cet ordre et
dit « integrations deployees en dernier » ; `make site` le lit, `deployer` ne
l'avait jamais lu. Meme registre desormais.
Deux politiques universelles se contredisaient : `client_pki` exemptait l'AC,
`client_metrique` refuse toute exemption. L'exemption confondait « ne pas
s'enroler » et « ne pas avoir de certificat ». `client_pki` distingue les deux
chemins : bootstrap pour les autres, EMISSION LOCALE sur l'AC. L'exemption
disparait, la doctrine reste.
Effet de bord instructif : `step ca bootstrap` ecrit aussi le defaults.json qui
porte l'URL de l'AC. En sautant le bootstrap on perdait l'information sans le
voir. `--ca-url` et `--root` sont explicites pour tous les hotes.
Mesure : infra-pki-01 et infra-dns-01 entierement deployees, aucun echec,
step-ca health=ok, powerdns resout la zone souveraine, et les deux hotes portent
un certificat de l'AC interne.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 23:57:01 -04:00
|
|
|
def _rang_des_couches() -> dict[str, int]:
|
|
|
|
|
"""Rang de chaque groupe dans `docs/couches-deploiement.yml`. {} si le fichier manque.
|
|
|
|
|
|
|
|
|
|
SOURCE UNIQUE avec `make site` : l'ordre d'un hote seul et celui de la reconstruction
|
|
|
|
|
complete doivent decouler du meme registre. Les tenir separement, c'est se garantir
|
|
|
|
|
qu'ils divergeront — et la divergence ne se voit qu'au deploiement, trop tard.
|
|
|
|
|
"""
|
|
|
|
|
fichier = Path(__file__).resolve().parent.parent / "docs" / "couches-deploiement.yml"
|
|
|
|
|
if not fichier.is_file():
|
|
|
|
|
return {}
|
|
|
|
|
try:
|
|
|
|
|
contenu = yaml.safe_load(fichier.read_text(encoding="utf-8")) or {}
|
|
|
|
|
except (OSError, yaml.YAMLError):
|
|
|
|
|
return {}
|
|
|
|
|
couches = contenu.get("couches") if isinstance(contenu, dict) else contenu
|
|
|
|
|
rangs: dict[str, int] = {}
|
|
|
|
|
for rang, couche in enumerate(couches or []):
|
|
|
|
|
for groupe in (couche or {}).get("groupes") or []:
|
|
|
|
|
rangs.setdefault(str(groupe), rang)
|
|
|
|
|
return rangs
|
|
|
|
|
|
|
|
|
|
|
2026-06-24 20:17:46 -04:00
|
|
|
def afficher_playbooks_hote(data: dict, host: str, playbook_dir: Path) -> None:
|
|
|
|
|
groups = groupes_hote(data, host)
|
|
|
|
|
if not groups:
|
|
|
|
|
raise ValueError(f"Hote absent de l'inventaire: {host}")
|
|
|
|
|
|
2026-07-02 09:32:20 -04:00
|
|
|
# Le SOCLE passe toujours en premier : il pose /etc/hosts (resolution interne),
|
|
|
|
|
# le durcissement, etc. Les integrations client_* et les services en dependent
|
|
|
|
|
# (ex. client_pki doit resoudre l'AC via /etc/hosts avant de bootstrapper).
|
deployer : l'ordre des couches, et l'autorite qui se signe elle-meme
`make deployer` triait les groupes alphabetiquement apres le socle :
`client_metrique` passait avant `serveur_step_ca`, donc exigeait un certificat
que l'autorite, pas encore deployee, ne pouvait pas avoir emis — sur l'hote de
l'AC lui-meme. `docs/couches-deploiement.yml` existe pour definir cet ordre et
dit « integrations deployees en dernier » ; `make site` le lit, `deployer` ne
l'avait jamais lu. Meme registre desormais.
Deux politiques universelles se contredisaient : `client_pki` exemptait l'AC,
`client_metrique` refuse toute exemption. L'exemption confondait « ne pas
s'enroler » et « ne pas avoir de certificat ». `client_pki` distingue les deux
chemins : bootstrap pour les autres, EMISSION LOCALE sur l'AC. L'exemption
disparait, la doctrine reste.
Effet de bord instructif : `step ca bootstrap` ecrit aussi le defaults.json qui
porte l'URL de l'AC. En sautant le bootstrap on perdait l'information sans le
voir. `--ca-url` et `--root` sont explicites pour tous les hotes.
Mesure : infra-pki-01 et infra-dns-01 entierement deployees, aucun echec,
step-ca health=ok, powerdns resout la zone souveraine, et les deux hotes portent
un certificat de l'AC interne.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 23:57:01 -04:00
|
|
|
#
|
|
|
|
|
# ENSUITE, l'ordre des COUCHES (docs/couches-deploiement.yml) — le meme registre que
|
|
|
|
|
# `make site`. Trier alphabetiquement mettait `client_metrique` avant
|
|
|
|
|
# `serveur_step_ca` : l'integration exigeait un certificat que l'autorite, pas encore
|
|
|
|
|
# deployee, ne pouvait pas avoir emis. Constate le 2026-08-06 sur les deux premieres
|
|
|
|
|
# VM, dont l'hote de l'AC lui-meme. La couche « integrations » dit en toutes lettres
|
|
|
|
|
# « deployees en dernier, quand leurs cibles sont debout » : `make deployer` l'ignorait.
|
2026-07-02 09:32:20 -04:00
|
|
|
socle = ["serveur_debian", "serveur_durci"]
|
deployer : l'ordre des couches, et l'autorite qui se signe elle-meme
`make deployer` triait les groupes alphabetiquement apres le socle :
`client_metrique` passait avant `serveur_step_ca`, donc exigeait un certificat
que l'autorite, pas encore deployee, ne pouvait pas avoir emis — sur l'hote de
l'AC lui-meme. `docs/couches-deploiement.yml` existe pour definir cet ordre et
dit « integrations deployees en dernier » ; `make site` le lit, `deployer` ne
l'avait jamais lu. Meme registre desormais.
Deux politiques universelles se contredisaient : `client_pki` exemptait l'AC,
`client_metrique` refuse toute exemption. L'exemption confondait « ne pas
s'enroler » et « ne pas avoir de certificat ». `client_pki` distingue les deux
chemins : bootstrap pour les autres, EMISSION LOCALE sur l'AC. L'exemption
disparait, la doctrine reste.
Effet de bord instructif : `step ca bootstrap` ecrit aussi le defaults.json qui
porte l'URL de l'AC. En sautant le bootstrap on perdait l'information sans le
voir. `--ca-url` et `--root` sont explicites pour tous les hotes.
Mesure : infra-pki-01 et infra-dns-01 entierement deployees, aucun echec,
step-ca health=ok, powerdns resout la zone souveraine, et les deux hotes portent
un certificat de l'AC interne.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 23:57:01 -04:00
|
|
|
couches = _rang_des_couches()
|
|
|
|
|
groups = sorted(groups, key=lambda g: (
|
|
|
|
|
socle.index(g) if g in socle else len(socle),
|
|
|
|
|
couches.get(g, len(couches)), # groupe hors registre : apres les couches connues
|
|
|
|
|
g,
|
|
|
|
|
))
|
2026-07-02 09:32:20 -04:00
|
|
|
|
2026-06-24 20:17:46 -04:00
|
|
|
missing_playbooks: list[str] = []
|
|
|
|
|
playbooks: list[Path] = []
|
|
|
|
|
|
|
|
|
|
for group in groups:
|
|
|
|
|
if not est_groupe_operationnel(group):
|
|
|
|
|
continue
|
|
|
|
|
playbook = playbook_dir / f"{group}.yml"
|
|
|
|
|
if playbook.exists():
|
|
|
|
|
playbooks.append(playbook)
|
|
|
|
|
else:
|
|
|
|
|
missing_playbooks.append(group)
|
|
|
|
|
|
|
|
|
|
if missing_playbooks:
|
|
|
|
|
missing = ", ".join(missing_playbooks)
|
|
|
|
|
raise ValueError(f"Playbook de groupe manquant pour: {missing}")
|
|
|
|
|
|
|
|
|
|
if not playbooks:
|
|
|
|
|
raise ValueError(f"Aucun playbook de groupe opérationnel pour: {host}")
|
|
|
|
|
|
|
|
|
|
for playbook in playbooks:
|
|
|
|
|
print(playbook)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def verifier_hote_actif(data: dict, host: str) -> None:
|
|
|
|
|
hosts = enfants(data).get(GROUPE_HOTES_ACTIFS, {}).get("hosts", {})
|
|
|
|
|
if host not in hosts:
|
|
|
|
|
raise ValueError(f"Hote non actif dans l'inventaire: {host}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def groupe_a_hote_actif(data: dict, group: str) -> bool:
|
|
|
|
|
active_hosts = set(enfants(data).get(GROUPE_HOTES_ACTIFS, {}).get("hosts", {}))
|
|
|
|
|
group_hosts = set(enfants(data).get(group, {}).get("hosts", {}))
|
|
|
|
|
return bool(active_hosts & group_hosts)
|
|
|
|
|
|
|
|
|
|
|
dependances : un ecosysteme minimal n'est pas un ecosysteme incomplet
Le controle de dependances a REFUSE le deploiement de patient 0 : client_journal requiert
serveur_loki, client_metrique requiert serveur_prometheus, serveur_forgejo requiert
serveur_postgresql et serveur_postfix. Quatre refus, une seule racine — le moteur suppose
que tout ecosysteme porte tous les services. Apres les intrants (P32) et les bases (P35),
troisieme manifestation en cinq jours. Et le mur que rencontrerait toute offre plus petite
que l'ecosysteme de reference.
UNE INTEGRATION UNIVERSELLE A BESOIN D'UN INTERLOCUTEUR. « Tout hote est mesure » est vrai
dans un ecosysteme qui porte un Prometheus ; ailleurs, la meme phrase pose sur chaque
machine un client qui n'a personne a qui parler. La regle est desormais DERIVEE : le
service central d'une integration est celui que le registre des dependances lui donne
deja. Rien de neuf a tenir a jour, donc rien de neuf a oublier.
Chezlepro -> diff VIDE (tous ses services existent, rien ne change)
patient 0 -> client_backup, client_pki, client_unbound
UNE EXIGENCE N'EST PAS TOUJOURS ABSOLUE. Deux notions manquaient au registre :
`sauf_si` l'exigence tombe sous condition (Forgejo + SQLite)
`utilise_si_present` un agrement, jamais bloquant (Forgejo notifie SI un MTA existe)
Les confondre obligeait une forge a deployer une pile courriel entiere pour exister.
ET LA LECON D'HIER A SERVI : trois lecteurs avaient besoin le meme jour de lire une
variable d'instance (P35, les clauses sauf_si, le generateur). Trois copies auraient
recommence ce qu'on venait de refermer. Il y en a UNE, dans inventory_rules.
make verifier 41/41 ; make ci 41/41 ; inventaire de Chezlepro identique.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 14:54:00 -04:00
|
|
|
def _exigence_levee(config: dict, required_group: str, group_vars: Path | None) -> bool:
|
|
|
|
|
"""La clause `sauf_si` retire-t-elle cette exigence pour CETTE instance ?
|
|
|
|
|
|
|
|
|
|
Une exigence n'est pas toujours absolue (2026-08-22) : `serveur_forgejo` exige un
|
|
|
|
|
serveur PostgreSQL — sauf quand l'instance a choisi `serveur_forgejo_bd: sqlite`, et sa
|
|
|
|
|
base tient alors dans un fichier. Sans cette lecture, un ecosysteme parfaitement
|
|
|
|
|
coherent se voyait refuser le deploiement au nom d'un service dont il n'a pas besoin.
|
|
|
|
|
|
|
|
|
|
La condition est lue dans les group_vars de l'instance, a defaut dans le defaut du
|
|
|
|
|
role : meme resolution que partout ailleurs (`inventory_rules.variable_effective`).
|
|
|
|
|
"""
|
|
|
|
|
clause = (config.get("sauf_si") or {}).get(required_group)
|
|
|
|
|
if not clause:
|
|
|
|
|
return False
|
|
|
|
|
valeur = variable_effective(str(clause.get("variable")), group_vars,
|
|
|
|
|
role_defaut=str(clause.get("role") or ""))
|
|
|
|
|
return valeur is not None and valeur == str(clause.get("vaut"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def verifier_dependances_groupes(data: dict, dependencies: dict, groups: list[str],
|
|
|
|
|
group_vars: Path | None = None) -> None:
|
2026-06-24 20:17:46 -04:00
|
|
|
missing: list[str] = []
|
|
|
|
|
|
|
|
|
|
for group in sorted(set(groups)):
|
dependances : un ecosysteme minimal n'est pas un ecosysteme incomplet
Le controle de dependances a REFUSE le deploiement de patient 0 : client_journal requiert
serveur_loki, client_metrique requiert serveur_prometheus, serveur_forgejo requiert
serveur_postgresql et serveur_postfix. Quatre refus, une seule racine — le moteur suppose
que tout ecosysteme porte tous les services. Apres les intrants (P32) et les bases (P35),
troisieme manifestation en cinq jours. Et le mur que rencontrerait toute offre plus petite
que l'ecosysteme de reference.
UNE INTEGRATION UNIVERSELLE A BESOIN D'UN INTERLOCUTEUR. « Tout hote est mesure » est vrai
dans un ecosysteme qui porte un Prometheus ; ailleurs, la meme phrase pose sur chaque
machine un client qui n'a personne a qui parler. La regle est desormais DERIVEE : le
service central d'une integration est celui que le registre des dependances lui donne
deja. Rien de neuf a tenir a jour, donc rien de neuf a oublier.
Chezlepro -> diff VIDE (tous ses services existent, rien ne change)
patient 0 -> client_backup, client_pki, client_unbound
UNE EXIGENCE N'EST PAS TOUJOURS ABSOLUE. Deux notions manquaient au registre :
`sauf_si` l'exigence tombe sous condition (Forgejo + SQLite)
`utilise_si_present` un agrement, jamais bloquant (Forgejo notifie SI un MTA existe)
Les confondre obligeait une forge a deployer une pile courriel entiere pour exister.
ET LA LECON D'HIER A SERVI : trois lecteurs avaient besoin le meme jour de lire une
variable d'instance (P35, les clauses sauf_si, le generateur). Trois copies auraient
recommence ce qu'on venait de refermer. Il y en a UNE, dans inventory_rules.
make verifier 41/41 ; make ci 41/41 ; inventaire de Chezlepro identique.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 14:54:00 -04:00
|
|
|
config = dependencies.get(group, {})
|
|
|
|
|
for required_group in config.get("requiert_groupes_actifs", []):
|
|
|
|
|
if _exigence_levee(config, required_group, group_vars):
|
|
|
|
|
continue
|
2026-06-24 20:17:46 -04:00
|
|
|
if not groupe_a_hote_actif(data, required_group):
|
|
|
|
|
missing.append(f"{group} requiert {required_group} actif")
|
dependances : un ecosysteme minimal n'est pas un ecosysteme incomplet
Le controle de dependances a REFUSE le deploiement de patient 0 : client_journal requiert
serveur_loki, client_metrique requiert serveur_prometheus, serveur_forgejo requiert
serveur_postgresql et serveur_postfix. Quatre refus, une seule racine — le moteur suppose
que tout ecosysteme porte tous les services. Apres les intrants (P32) et les bases (P35),
troisieme manifestation en cinq jours. Et le mur que rencontrerait toute offre plus petite
que l'ecosysteme de reference.
UNE INTEGRATION UNIVERSELLE A BESOIN D'UN INTERLOCUTEUR. « Tout hote est mesure » est vrai
dans un ecosysteme qui porte un Prometheus ; ailleurs, la meme phrase pose sur chaque
machine un client qui n'a personne a qui parler. La regle est desormais DERIVEE : le
service central d'une integration est celui que le registre des dependances lui donne
deja. Rien de neuf a tenir a jour, donc rien de neuf a oublier.
Chezlepro -> diff VIDE (tous ses services existent, rien ne change)
patient 0 -> client_backup, client_pki, client_unbound
UNE EXIGENCE N'EST PAS TOUJOURS ABSOLUE. Deux notions manquaient au registre :
`sauf_si` l'exigence tombe sous condition (Forgejo + SQLite)
`utilise_si_present` un agrement, jamais bloquant (Forgejo notifie SI un MTA existe)
Les confondre obligeait une forge a deployer une pile courriel entiere pour exister.
ET LA LECON D'HIER A SERVI : trois lecteurs avaient besoin le meme jour de lire une
variable d'instance (P35, les clauses sauf_si, le generateur). Trois copies auraient
recommence ce qu'on venait de refermer. Il y en a UNE, dans inventory_rules.
make verifier 41/41 ; make ci 41/41 ; inventaire de Chezlepro identique.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 14:54:00 -04:00
|
|
|
# `utilise_si_present` ne bloque JAMAIS : c'est un agrement, pas une exigence.
|
|
|
|
|
# Les confondre obligeait une forge a deployer une pile courriel pour exister.
|
2026-06-24 20:17:46 -04:00
|
|
|
|
|
|
|
|
if missing:
|
|
|
|
|
raise ValueError("Dependances manquantes: " + "; ".join(missing))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def exiger_dependances(dependencies: dict, path: Path | None) -> None:
|
|
|
|
|
if not path:
|
|
|
|
|
raise ValueError("Fichier de dependances requis: utiliser --dependances.")
|
|
|
|
|
if not dependencies:
|
|
|
|
|
raise ValueError(f"Aucune dependance chargee depuis {path}.")
|
|
|
|
|
|
|
|
|
|
|
dependances : un ecosysteme minimal n'est pas un ecosysteme incomplet
Le controle de dependances a REFUSE le deploiement de patient 0 : client_journal requiert
serveur_loki, client_metrique requiert serveur_prometheus, serveur_forgejo requiert
serveur_postgresql et serveur_postfix. Quatre refus, une seule racine — le moteur suppose
que tout ecosysteme porte tous les services. Apres les intrants (P32) et les bases (P35),
troisieme manifestation en cinq jours. Et le mur que rencontrerait toute offre plus petite
que l'ecosysteme de reference.
UNE INTEGRATION UNIVERSELLE A BESOIN D'UN INTERLOCUTEUR. « Tout hote est mesure » est vrai
dans un ecosysteme qui porte un Prometheus ; ailleurs, la meme phrase pose sur chaque
machine un client qui n'a personne a qui parler. La regle est desormais DERIVEE : le
service central d'une integration est celui que le registre des dependances lui donne
deja. Rien de neuf a tenir a jour, donc rien de neuf a oublier.
Chezlepro -> diff VIDE (tous ses services existent, rien ne change)
patient 0 -> client_backup, client_pki, client_unbound
UNE EXIGENCE N'EST PAS TOUJOURS ABSOLUE. Deux notions manquaient au registre :
`sauf_si` l'exigence tombe sous condition (Forgejo + SQLite)
`utilise_si_present` un agrement, jamais bloquant (Forgejo notifie SI un MTA existe)
Les confondre obligeait une forge a deployer une pile courriel entiere pour exister.
ET LA LECON D'HIER A SERVI : trois lecteurs avaient besoin le meme jour de lire une
variable d'instance (P35, les clauses sauf_si, le generateur). Trois copies auraient
recommence ce qu'on venait de refermer. Il y en a UNE, dans inventory_rules.
make verifier 41/41 ; make ci 41/41 ; inventaire de Chezlepro identique.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 14:54:00 -04:00
|
|
|
def verifier_dependances_hote(data: dict, dependencies: dict, host: str,
|
|
|
|
|
group_vars: Path | None = None) -> None:
|
2026-06-24 20:17:46 -04:00
|
|
|
groups = [group for group in groupes_hote(data, host) if est_groupe_operationnel(group)]
|
dependances : un ecosysteme minimal n'est pas un ecosysteme incomplet
Le controle de dependances a REFUSE le deploiement de patient 0 : client_journal requiert
serveur_loki, client_metrique requiert serveur_prometheus, serveur_forgejo requiert
serveur_postgresql et serveur_postfix. Quatre refus, une seule racine — le moteur suppose
que tout ecosysteme porte tous les services. Apres les intrants (P32) et les bases (P35),
troisieme manifestation en cinq jours. Et le mur que rencontrerait toute offre plus petite
que l'ecosysteme de reference.
UNE INTEGRATION UNIVERSELLE A BESOIN D'UN INTERLOCUTEUR. « Tout hote est mesure » est vrai
dans un ecosysteme qui porte un Prometheus ; ailleurs, la meme phrase pose sur chaque
machine un client qui n'a personne a qui parler. La regle est desormais DERIVEE : le
service central d'une integration est celui que le registre des dependances lui donne
deja. Rien de neuf a tenir a jour, donc rien de neuf a oublier.
Chezlepro -> diff VIDE (tous ses services existent, rien ne change)
patient 0 -> client_backup, client_pki, client_unbound
UNE EXIGENCE N'EST PAS TOUJOURS ABSOLUE. Deux notions manquaient au registre :
`sauf_si` l'exigence tombe sous condition (Forgejo + SQLite)
`utilise_si_present` un agrement, jamais bloquant (Forgejo notifie SI un MTA existe)
Les confondre obligeait une forge a deployer une pile courriel entiere pour exister.
ET LA LECON D'HIER A SERVI : trois lecteurs avaient besoin le meme jour de lire une
variable d'instance (P35, les clauses sauf_si, le generateur). Trois copies auraient
recommence ce qu'on venait de refermer. Il y en a UNE, dans inventory_rules.
make verifier 41/41 ; make ci 41/41 ; inventaire de Chezlepro identique.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 14:54:00 -04:00
|
|
|
verifier_dependances_groupes(data, dependencies, groups, group_vars)
|
2026-06-24 20:17:46 -04:00
|
|
|
|
|
|
|
|
|
dependances : un ecosysteme minimal n'est pas un ecosysteme incomplet
Le controle de dependances a REFUSE le deploiement de patient 0 : client_journal requiert
serveur_loki, client_metrique requiert serveur_prometheus, serveur_forgejo requiert
serveur_postgresql et serveur_postfix. Quatre refus, une seule racine — le moteur suppose
que tout ecosysteme porte tous les services. Apres les intrants (P32) et les bases (P35),
troisieme manifestation en cinq jours. Et le mur que rencontrerait toute offre plus petite
que l'ecosysteme de reference.
UNE INTEGRATION UNIVERSELLE A BESOIN D'UN INTERLOCUTEUR. « Tout hote est mesure » est vrai
dans un ecosysteme qui porte un Prometheus ; ailleurs, la meme phrase pose sur chaque
machine un client qui n'a personne a qui parler. La regle est desormais DERIVEE : le
service central d'une integration est celui que le registre des dependances lui donne
deja. Rien de neuf a tenir a jour, donc rien de neuf a oublier.
Chezlepro -> diff VIDE (tous ses services existent, rien ne change)
patient 0 -> client_backup, client_pki, client_unbound
UNE EXIGENCE N'EST PAS TOUJOURS ABSOLUE. Deux notions manquaient au registre :
`sauf_si` l'exigence tombe sous condition (Forgejo + SQLite)
`utilise_si_present` un agrement, jamais bloquant (Forgejo notifie SI un MTA existe)
Les confondre obligeait une forge a deployer une pile courriel entiere pour exister.
ET LA LECON D'HIER A SERVI : trois lecteurs avaient besoin le meme jour de lire une
variable d'instance (P35, les clauses sauf_si, le generateur). Trois copies auraient
recommence ce qu'on venait de refermer. Il y en a UNE, dans inventory_rules.
make verifier 41/41 ; make ci 41/41 ; inventaire de Chezlepro identique.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 14:54:00 -04:00
|
|
|
def verifier_dependances_actives(data: dict, dependencies: dict,
|
|
|
|
|
group_vars: Path | None = None) -> None:
|
2026-06-24 20:17:46 -04:00
|
|
|
active_hosts = set(enfants(data).get(GROUPE_HOTES_ACTIFS, {}).get("hosts", {}))
|
|
|
|
|
active_groups: set[str] = set()
|
|
|
|
|
|
|
|
|
|
for group, group_data in enfants(data).items():
|
|
|
|
|
if not est_groupe_operationnel(group):
|
|
|
|
|
continue
|
|
|
|
|
if active_hosts & set(group_data.get("hosts", {})):
|
|
|
|
|
active_groups.add(group)
|
|
|
|
|
|
dependances : un ecosysteme minimal n'est pas un ecosysteme incomplet
Le controle de dependances a REFUSE le deploiement de patient 0 : client_journal requiert
serveur_loki, client_metrique requiert serveur_prometheus, serveur_forgejo requiert
serveur_postgresql et serveur_postfix. Quatre refus, une seule racine — le moteur suppose
que tout ecosysteme porte tous les services. Apres les intrants (P32) et les bases (P35),
troisieme manifestation en cinq jours. Et le mur que rencontrerait toute offre plus petite
que l'ecosysteme de reference.
UNE INTEGRATION UNIVERSELLE A BESOIN D'UN INTERLOCUTEUR. « Tout hote est mesure » est vrai
dans un ecosysteme qui porte un Prometheus ; ailleurs, la meme phrase pose sur chaque
machine un client qui n'a personne a qui parler. La regle est desormais DERIVEE : le
service central d'une integration est celui que le registre des dependances lui donne
deja. Rien de neuf a tenir a jour, donc rien de neuf a oublier.
Chezlepro -> diff VIDE (tous ses services existent, rien ne change)
patient 0 -> client_backup, client_pki, client_unbound
UNE EXIGENCE N'EST PAS TOUJOURS ABSOLUE. Deux notions manquaient au registre :
`sauf_si` l'exigence tombe sous condition (Forgejo + SQLite)
`utilise_si_present` un agrement, jamais bloquant (Forgejo notifie SI un MTA existe)
Les confondre obligeait une forge a deployer une pile courriel entiere pour exister.
ET LA LECON D'HIER A SERVI : trois lecteurs avaient besoin le meme jour de lire une
variable d'instance (P35, les clauses sauf_si, le generateur). Trois copies auraient
recommence ce qu'on venait de refermer. Il y en a UNE, dans inventory_rules.
make verifier 41/41 ; make ci 41/41 ; inventaire de Chezlepro identique.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 14:54:00 -04:00
|
|
|
verifier_dependances_groupes(data, dependencies, sorted(active_groups), group_vars)
|
2026-06-24 20:17:46 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def verifier_dependances_connues(data: dict, dependencies: dict, playbook_dir: Path | None) -> None:
|
|
|
|
|
for group, config in dependencies.items():
|
|
|
|
|
if playbook_dir and not (playbook_dir / f"{group}.yml").exists():
|
|
|
|
|
raise ValueError(f"Playbook de groupe manquant pour dependance: {group}")
|
|
|
|
|
for required_group in config.get("requiert_groupes_actifs", []):
|
|
|
|
|
if playbook_dir and not (playbook_dir / f"{required_group}.yml").exists():
|
|
|
|
|
raise ValueError(f"Playbook de groupe manquant pour prerequis: {required_group}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def verifier_coherence_hotes(data: dict) -> None:
|
|
|
|
|
vmids: dict[str, str] = {}
|
|
|
|
|
|
|
|
|
|
for host in tous_les_hotes(data):
|
|
|
|
|
groups = groupes_hote(data, host)
|
|
|
|
|
state_groups = [group for group in groups if group in GROUPES_ETAT_HOTE]
|
|
|
|
|
operational_groups = [group for group in groups if est_groupe_operationnel(group)]
|
|
|
|
|
vars_for_host = variables_hote(data, host)
|
|
|
|
|
|
|
|
|
|
if not state_groups and not operational_groups:
|
|
|
|
|
continue
|
|
|
|
|
if len(state_groups) != 1:
|
|
|
|
|
raise ValueError(
|
|
|
|
|
f"{host} doit appartenir exactement a un groupe d'etat: "
|
|
|
|
|
f"{GROUPE_HOTES_ACTIFS} ou {GROUPE_HOTES_PLANIFIES}"
|
|
|
|
|
)
|
|
|
|
|
if not operational_groups:
|
|
|
|
|
raise ValueError(f"{host} doit appartenir a au moins un groupe operationnel.")
|
|
|
|
|
if GROUPE_HOTES_ACTIFS in state_groups and not vars_for_host.get("ansible_host"):
|
|
|
|
|
raise ValueError(f"{host} est actif mais n'a pas ansible_host.")
|
|
|
|
|
|
|
|
|
|
vmid = vars_for_host.get("proxmox_vmid")
|
|
|
|
|
if vmid in (None, ""):
|
|
|
|
|
continue
|
|
|
|
|
vmid_key = str(vmid)
|
|
|
|
|
if vmid_key in vmids and vmids[vmid_key] != host:
|
|
|
|
|
raise ValueError(f"VMID en double: {vmid_key} pour {vmids[vmid_key]} et {host}")
|
|
|
|
|
vmids[vmid_key] = host
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def verifier_playbooks_groupes(data: dict, playbook_dir: Path) -> None:
|
|
|
|
|
missing_playbooks: list[str] = []
|
|
|
|
|
|
|
|
|
|
for group, group_data in enfants(data).items():
|
|
|
|
|
hosts = group_data.get("hosts", {})
|
|
|
|
|
if not hosts:
|
|
|
|
|
continue
|
|
|
|
|
if not est_groupe_operationnel(group):
|
|
|
|
|
continue
|
|
|
|
|
playbook = playbook_dir / f"{group}.yml"
|
|
|
|
|
if not playbook.exists():
|
|
|
|
|
missing_playbooks.append(group)
|
|
|
|
|
|
|
|
|
|
if missing_playbooks:
|
|
|
|
|
missing = ", ".join(sorted(missing_playbooks))
|
|
|
|
|
raise ValueError(f"Playbook de groupe manquant pour: {missing}")
|
|
|
|
|
|
|
|
|
|
verifier_coherence_hotes(data)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main() -> int:
|
|
|
|
|
parser = argparse.ArgumentParser(description="Gere les hotes d'un inventaire Ansible statique Set-OPS.")
|
|
|
|
|
parser.add_argument("--inventaire", required=True, type=Path)
|
|
|
|
|
parser.add_argument("--dependances", type=Path)
|
|
|
|
|
|
|
|
|
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
|
|
|
|
|
|
|
|
add_parser = subparsers.add_parser("ajouter", help="Ajoute ou met a jour un hote dans un ou plusieurs groupes.")
|
|
|
|
|
add_parser.add_argument("--hote", required=True)
|
|
|
|
|
add_parser.add_argument("--groupes", required=True)
|
|
|
|
|
add_parser.add_argument("--adresse-ip")
|
|
|
|
|
add_parser.add_argument("--utilisateur-ansible")
|
|
|
|
|
add_parser.add_argument("--vmid")
|
|
|
|
|
add_parser.add_argument("--dossier-playbooks", type=Path)
|
|
|
|
|
|
|
|
|
|
groups_parser = subparsers.add_parser("definir-groupes", help="Remplace l'appartenance aux groupes pour un hote.")
|
|
|
|
|
groups_parser.add_argument("--hote", required=True)
|
|
|
|
|
groups_parser.add_argument("--groupes", required=True)
|
|
|
|
|
groups_parser.add_argument("--dossier-playbooks", type=Path)
|
|
|
|
|
|
|
|
|
|
show_parser = subparsers.add_parser("afficher", help="Affiche les groupes et variables d'un hote.")
|
|
|
|
|
show_parser.add_argument("--hote", required=True)
|
|
|
|
|
|
|
|
|
|
proxmox_parser = subparsers.add_parser(
|
|
|
|
|
"parametres-proxmox",
|
|
|
|
|
help="Emet les parametres de clonage Proxmox d'un hote (lus depuis l'inventaire).",
|
|
|
|
|
)
|
|
|
|
|
proxmox_parser.add_argument("--hote", required=True)
|
|
|
|
|
|
|
|
|
|
playbooks_parser = subparsers.add_parser("playbooks", help="Affiche les playbooks de groupes pour un hote.")
|
|
|
|
|
playbooks_parser.add_argument("--hote", required=True)
|
|
|
|
|
playbooks_parser.add_argument("--dossier-playbooks", required=True, type=Path)
|
|
|
|
|
|
|
|
|
|
active_parser = subparsers.add_parser("verifier-actif", help="Verifie qu'un hote est actif.")
|
|
|
|
|
active_parser.add_argument("--hote", required=True)
|
|
|
|
|
|
2026-07-07 03:08:09 -04:00
|
|
|
subparsers.add_parser("lister-actifs", help="Liste les hotes actifs, un par ligne (pour la creation/deploiement de flotte).")
|
|
|
|
|
|
2026-06-24 20:17:46 -04:00
|
|
|
host_dependencies_parser = subparsers.add_parser(
|
|
|
|
|
"verifier-dependances-hote",
|
|
|
|
|
help="Verifie les prerequis actifs requis par les groupes d'un hote.",
|
|
|
|
|
)
|
|
|
|
|
host_dependencies_parser.add_argument("--hote", required=True)
|
|
|
|
|
|
|
|
|
|
group_dependencies_parser = subparsers.add_parser(
|
|
|
|
|
"verifier-dependances-groupe",
|
|
|
|
|
help="Verifie les prerequis actifs requis par un groupe.",
|
|
|
|
|
)
|
|
|
|
|
group_dependencies_parser.add_argument("--groupe", required=True)
|
|
|
|
|
|
|
|
|
|
verify_parser = subparsers.add_parser(
|
|
|
|
|
"verifier-playbooks",
|
|
|
|
|
help="Verifie que les groupes operationnels avec hotes ont un playbook homonyme.",
|
|
|
|
|
)
|
|
|
|
|
verify_parser.add_argument("--dossier-playbooks", required=True, type=Path)
|
|
|
|
|
|
|
|
|
|
dependencies_parser = subparsers.add_parser(
|
|
|
|
|
"verifier-dependances",
|
|
|
|
|
help="Verifie le registre de dependances et les prerequis des groupes actifs.",
|
|
|
|
|
)
|
|
|
|
|
dependencies_parser.add_argument("--dossier-playbooks", required=True, type=Path)
|
|
|
|
|
|
|
|
|
|
args = parser.parse_args()
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
data = charger_inventaire(args.inventaire)
|
|
|
|
|
dependencies = charger_dependances(args.dependances)
|
|
|
|
|
|
|
|
|
|
if args.command == "ajouter":
|
|
|
|
|
ajouter_hote(
|
|
|
|
|
data,
|
|
|
|
|
args.hote,
|
|
|
|
|
analyser_groupes(args.groupes),
|
|
|
|
|
args.adresse_ip,
|
|
|
|
|
args.utilisateur_ansible,
|
|
|
|
|
args.vmid,
|
|
|
|
|
)
|
|
|
|
|
if args.dossier_playbooks:
|
|
|
|
|
verifier_playbooks_groupes(data, args.dossier_playbooks)
|
|
|
|
|
if args.dependances:
|
|
|
|
|
verifier_dependances_connues(data, dependencies, args.dossier_playbooks)
|
dependances : un ecosysteme minimal n'est pas un ecosysteme incomplet
Le controle de dependances a REFUSE le deploiement de patient 0 : client_journal requiert
serveur_loki, client_metrique requiert serveur_prometheus, serveur_forgejo requiert
serveur_postgresql et serveur_postfix. Quatre refus, une seule racine — le moteur suppose
que tout ecosysteme porte tous les services. Apres les intrants (P32) et les bases (P35),
troisieme manifestation en cinq jours. Et le mur que rencontrerait toute offre plus petite
que l'ecosysteme de reference.
UNE INTEGRATION UNIVERSELLE A BESOIN D'UN INTERLOCUTEUR. « Tout hote est mesure » est vrai
dans un ecosysteme qui porte un Prometheus ; ailleurs, la meme phrase pose sur chaque
machine un client qui n'a personne a qui parler. La regle est desormais DERIVEE : le
service central d'une integration est celui que le registre des dependances lui donne
deja. Rien de neuf a tenir a jour, donc rien de neuf a oublier.
Chezlepro -> diff VIDE (tous ses services existent, rien ne change)
patient 0 -> client_backup, client_pki, client_unbound
UNE EXIGENCE N'EST PAS TOUJOURS ABSOLUE. Deux notions manquaient au registre :
`sauf_si` l'exigence tombe sous condition (Forgejo + SQLite)
`utilise_si_present` un agrement, jamais bloquant (Forgejo notifie SI un MTA existe)
Les confondre obligeait une forge a deployer une pile courriel entiere pour exister.
ET LA LECON D'HIER A SERVI : trois lecteurs avaient besoin le meme jour de lire une
variable d'instance (P35, les clauses sauf_si, le generateur). Trois copies auraient
recommence ce qu'on venait de refermer. Il y en a UNE, dans inventory_rules.
make verifier 41/41 ; make ci 41/41 ; inventaire de Chezlepro identique.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 14:54:00 -04:00
|
|
|
verifier_dependances_actives(
|
|
|
|
|
data, dependencies, Path(args.inventaire).parent / "group_vars")
|
2026-06-24 20:17:46 -04:00
|
|
|
ecrire_inventaire(args.inventaire, data)
|
|
|
|
|
elif args.command == "definir-groupes":
|
|
|
|
|
definir_groupes_hote(data, args.hote, analyser_groupes(args.groupes))
|
|
|
|
|
if args.dossier_playbooks:
|
|
|
|
|
verifier_playbooks_groupes(data, args.dossier_playbooks)
|
|
|
|
|
if args.dependances:
|
|
|
|
|
verifier_dependances_connues(data, dependencies, args.dossier_playbooks)
|
dependances : un ecosysteme minimal n'est pas un ecosysteme incomplet
Le controle de dependances a REFUSE le deploiement de patient 0 : client_journal requiert
serveur_loki, client_metrique requiert serveur_prometheus, serveur_forgejo requiert
serveur_postgresql et serveur_postfix. Quatre refus, une seule racine — le moteur suppose
que tout ecosysteme porte tous les services. Apres les intrants (P32) et les bases (P35),
troisieme manifestation en cinq jours. Et le mur que rencontrerait toute offre plus petite
que l'ecosysteme de reference.
UNE INTEGRATION UNIVERSELLE A BESOIN D'UN INTERLOCUTEUR. « Tout hote est mesure » est vrai
dans un ecosysteme qui porte un Prometheus ; ailleurs, la meme phrase pose sur chaque
machine un client qui n'a personne a qui parler. La regle est desormais DERIVEE : le
service central d'une integration est celui que le registre des dependances lui donne
deja. Rien de neuf a tenir a jour, donc rien de neuf a oublier.
Chezlepro -> diff VIDE (tous ses services existent, rien ne change)
patient 0 -> client_backup, client_pki, client_unbound
UNE EXIGENCE N'EST PAS TOUJOURS ABSOLUE. Deux notions manquaient au registre :
`sauf_si` l'exigence tombe sous condition (Forgejo + SQLite)
`utilise_si_present` un agrement, jamais bloquant (Forgejo notifie SI un MTA existe)
Les confondre obligeait une forge a deployer une pile courriel entiere pour exister.
ET LA LECON D'HIER A SERVI : trois lecteurs avaient besoin le meme jour de lire une
variable d'instance (P35, les clauses sauf_si, le generateur). Trois copies auraient
recommence ce qu'on venait de refermer. Il y en a UNE, dans inventory_rules.
make verifier 41/41 ; make ci 41/41 ; inventaire de Chezlepro identique.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 14:54:00 -04:00
|
|
|
verifier_dependances_actives(
|
|
|
|
|
data, dependencies, Path(args.inventaire).parent / "group_vars")
|
2026-06-24 20:17:46 -04:00
|
|
|
ecrire_inventaire(args.inventaire, data)
|
|
|
|
|
elif args.command == "afficher":
|
|
|
|
|
afficher_hote(data, args.hote)
|
|
|
|
|
elif args.command == "parametres-proxmox":
|
dns : client_unbound universel, et un resolveur d'amorcage
Une VM ne peut pas s'installer sans resoudre des noms. PowerDNS repond
UNIQUEMENT pour la zone souveraine et ne recurse pour personne : il manquait un
resolveur recursif. `client_unbound` est l'outil ecrit pour ca — il rejoint
`client_journal` et `client_metrique` parmi les integrations universelles.
`infra-dns-01` est exempte (PowerDNS occupe son port 53), et
`serveur_powerdns_listen_addresses` passe de 0.0.0.0 a l'adresse de l'hote pour
laisser 127.0.0.1:53 libre. Mais l'appartenance au groupe est AUSSI ce qui ouvre
le port 53 a la frontiere : en exemptant la machine, je lui retirais le droit de
resoudre. `serveur_powerdns` declare donc son propre flux sortant — il ne
recurse pour personne, mais doit resoudre pour lui-meme.
Nouvel intrant `dns_amorcage`, derive jusqu'a `make creer-vm`. Cloud-init
l'ecrit bien mais sans effet : `dns-nameservers` exige `resolvconf`, absent du
gabarit, et installer resolvconf demande apt, qui demande la resolution.
`serveur_debian` pose donc le resolveur en pre_tasks, avant le premier apt, avec
une garde qui respecte la bascule ulterieure de client_unbound.
Defaut corrige en chemin : `_intrants_communs()` lisait `instance/` en dur ; le
chemin derive maintenant de l'inventaire recu, et un test le prouve.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 21:41:58 -04:00
|
|
|
for ligne in parametres_proxmox_hote(data, args.hote, args.inventaire):
|
2026-06-24 20:17:46 -04:00
|
|
|
print(ligne)
|
|
|
|
|
elif args.command == "playbooks":
|
|
|
|
|
afficher_playbooks_hote(data, args.hote, args.dossier_playbooks)
|
|
|
|
|
elif args.command == "verifier-actif":
|
|
|
|
|
verifier_hote_actif(data, args.hote)
|
2026-07-07 03:08:09 -04:00
|
|
|
elif args.command == "lister-actifs":
|
|
|
|
|
for hote in sorted(enfants(data).get(GROUPE_HOTES_ACTIFS, {}).get("hosts", {}) or {}):
|
|
|
|
|
print(hote)
|
2026-06-24 20:17:46 -04:00
|
|
|
elif args.command == "verifier-dependances-hote":
|
|
|
|
|
exiger_dependances(dependencies, args.dependances)
|
dependances : un ecosysteme minimal n'est pas un ecosysteme incomplet
Le controle de dependances a REFUSE le deploiement de patient 0 : client_journal requiert
serveur_loki, client_metrique requiert serveur_prometheus, serveur_forgejo requiert
serveur_postgresql et serveur_postfix. Quatre refus, une seule racine — le moteur suppose
que tout ecosysteme porte tous les services. Apres les intrants (P32) et les bases (P35),
troisieme manifestation en cinq jours. Et le mur que rencontrerait toute offre plus petite
que l'ecosysteme de reference.
UNE INTEGRATION UNIVERSELLE A BESOIN D'UN INTERLOCUTEUR. « Tout hote est mesure » est vrai
dans un ecosysteme qui porte un Prometheus ; ailleurs, la meme phrase pose sur chaque
machine un client qui n'a personne a qui parler. La regle est desormais DERIVEE : le
service central d'une integration est celui que le registre des dependances lui donne
deja. Rien de neuf a tenir a jour, donc rien de neuf a oublier.
Chezlepro -> diff VIDE (tous ses services existent, rien ne change)
patient 0 -> client_backup, client_pki, client_unbound
UNE EXIGENCE N'EST PAS TOUJOURS ABSOLUE. Deux notions manquaient au registre :
`sauf_si` l'exigence tombe sous condition (Forgejo + SQLite)
`utilise_si_present` un agrement, jamais bloquant (Forgejo notifie SI un MTA existe)
Les confondre obligeait une forge a deployer une pile courriel entiere pour exister.
ET LA LECON D'HIER A SERVI : trois lecteurs avaient besoin le meme jour de lire une
variable d'instance (P35, les clauses sauf_si, le generateur). Trois copies auraient
recommence ce qu'on venait de refermer. Il y en a UNE, dans inventory_rules.
make verifier 41/41 ; make ci 41/41 ; inventaire de Chezlepro identique.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 14:54:00 -04:00
|
|
|
verifier_dependances_hote(data, dependencies, args.hote,
|
|
|
|
|
Path(args.inventaire).parent / 'group_vars')
|
2026-06-24 20:17:46 -04:00
|
|
|
elif args.command == "verifier-dependances-groupe":
|
|
|
|
|
exiger_dependances(dependencies, args.dependances)
|
dependances : un ecosysteme minimal n'est pas un ecosysteme incomplet
Le controle de dependances a REFUSE le deploiement de patient 0 : client_journal requiert
serveur_loki, client_metrique requiert serveur_prometheus, serveur_forgejo requiert
serveur_postgresql et serveur_postfix. Quatre refus, une seule racine — le moteur suppose
que tout ecosysteme porte tous les services. Apres les intrants (P32) et les bases (P35),
troisieme manifestation en cinq jours. Et le mur que rencontrerait toute offre plus petite
que l'ecosysteme de reference.
UNE INTEGRATION UNIVERSELLE A BESOIN D'UN INTERLOCUTEUR. « Tout hote est mesure » est vrai
dans un ecosysteme qui porte un Prometheus ; ailleurs, la meme phrase pose sur chaque
machine un client qui n'a personne a qui parler. La regle est desormais DERIVEE : le
service central d'une integration est celui que le registre des dependances lui donne
deja. Rien de neuf a tenir a jour, donc rien de neuf a oublier.
Chezlepro -> diff VIDE (tous ses services existent, rien ne change)
patient 0 -> client_backup, client_pki, client_unbound
UNE EXIGENCE N'EST PAS TOUJOURS ABSOLUE. Deux notions manquaient au registre :
`sauf_si` l'exigence tombe sous condition (Forgejo + SQLite)
`utilise_si_present` un agrement, jamais bloquant (Forgejo notifie SI un MTA existe)
Les confondre obligeait une forge a deployer une pile courriel entiere pour exister.
ET LA LECON D'HIER A SERVI : trois lecteurs avaient besoin le meme jour de lire une
variable d'instance (P35, les clauses sauf_si, le generateur). Trois copies auraient
recommence ce qu'on venait de refermer. Il y en a UNE, dans inventory_rules.
make verifier 41/41 ; make ci 41/41 ; inventaire de Chezlepro identique.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 14:54:00 -04:00
|
|
|
verifier_dependances_groupes(
|
|
|
|
|
data, dependencies, [args.groupe],
|
|
|
|
|
Path(args.inventaire).parent / "group_vars")
|
2026-06-24 20:17:46 -04:00
|
|
|
elif args.command == "verifier-playbooks":
|
|
|
|
|
verifier_playbooks_groupes(data, args.dossier_playbooks)
|
|
|
|
|
elif args.command == "verifier-dependances":
|
|
|
|
|
exiger_dependances(dependencies, args.dependances)
|
|
|
|
|
verifier_dependances_connues(data, dependencies, args.dossier_playbooks)
|
dependances : un ecosysteme minimal n'est pas un ecosysteme incomplet
Le controle de dependances a REFUSE le deploiement de patient 0 : client_journal requiert
serveur_loki, client_metrique requiert serveur_prometheus, serveur_forgejo requiert
serveur_postgresql et serveur_postfix. Quatre refus, une seule racine — le moteur suppose
que tout ecosysteme porte tous les services. Apres les intrants (P32) et les bases (P35),
troisieme manifestation en cinq jours. Et le mur que rencontrerait toute offre plus petite
que l'ecosysteme de reference.
UNE INTEGRATION UNIVERSELLE A BESOIN D'UN INTERLOCUTEUR. « Tout hote est mesure » est vrai
dans un ecosysteme qui porte un Prometheus ; ailleurs, la meme phrase pose sur chaque
machine un client qui n'a personne a qui parler. La regle est desormais DERIVEE : le
service central d'une integration est celui que le registre des dependances lui donne
deja. Rien de neuf a tenir a jour, donc rien de neuf a oublier.
Chezlepro -> diff VIDE (tous ses services existent, rien ne change)
patient 0 -> client_backup, client_pki, client_unbound
UNE EXIGENCE N'EST PAS TOUJOURS ABSOLUE. Deux notions manquaient au registre :
`sauf_si` l'exigence tombe sous condition (Forgejo + SQLite)
`utilise_si_present` un agrement, jamais bloquant (Forgejo notifie SI un MTA existe)
Les confondre obligeait une forge a deployer une pile courriel entiere pour exister.
ET LA LECON D'HIER A SERVI : trois lecteurs avaient besoin le meme jour de lire une
variable d'instance (P35, les clauses sauf_si, le generateur). Trois copies auraient
recommence ce qu'on venait de refermer. Il y en a UNE, dans inventory_rules.
make verifier 41/41 ; make ci 41/41 ; inventaire de Chezlepro identique.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 14:54:00 -04:00
|
|
|
verifier_dependances_actives(
|
|
|
|
|
data, dependencies, Path(args.inventaire).parent / "group_vars")
|
2026-06-24 20:17:46 -04:00
|
|
|
except Exception as exc:
|
|
|
|
|
print(f"erreur: {exc}", file=sys.stderr)
|
|
|
|
|
return 2
|
|
|
|
|
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
raise SystemExit(main())
|