Set-OPS-Public/Makefile

1590 lines
84 KiB
Makefile
Raw Normal View History

SHELL := /usr/bin/env bash
export ANSIBLE_HOME ?= $(CURDIR)/.ansible
export ANSIBLE_LOCAL_TEMP ?= $(CURDIR)/.ansible/tmp
export ANSIBLE_SSH_CONTROL_PATH_DIR ?= $(CURDIR)/.ansible/cp
export ANSIBLE_SSH_ARGS ?= -F /dev/null -o ControlMaster=no
export SETOPS_INSTANCE ?= instance
voutes : une voute, une cle — separer avant de distribuer Decision de l'exploitant : chaque runner est maitre de sa voute et en detient la cle. C'est ce qui rend un runner autonome, donc ce qui rend l'emancipation atteignable. Elle exigeait un prealable, mesure ce matin : UN SEUL mot de passe ouvrait les SIX voutes de la flotte, celle de la fabric comprise. DISTRIBUER AVANT DE SEPARER AURAIT ETE PIRE QUE LE STATU QUO : poser « la » cle sur chaque runner rendait chaque runner capable d'ouvrir les autres. Compromettre le plus petit locataire donnait les secrets de l'hebergeur. Cinq cles, une par ecosysteme, sous ~/.config/setops-vault-<depot>. Verification croisee apres rechiffrement : la diagonale, et rien qu'elle. L'ancienne cle maitresse n'ouvre plus aucune des six. UN SEUL MOT DE PASSE NE POUVAIT PLUS SUFFIRE, et pas pour la raison qu'on croit : `cloner_vm_debian.yml` charge la voute du TENANT puis celle de l'UNDERLAY dans la meme execution. `ANSIBLE_VAULT_IDENTITY_LIST` en porte plusieurs et les essaie toutes — un seul export suffit pour les 28 appels a ansible-playbook, sans en toucher un seul. La liste se derive dans scripts/voutes.py. CE QUI BORNE LE POUVOIR N'EST PAS LA LISTE MAIS LA PRESENCE DES FICHIERS. Sur le poste, toutes les cles sont la — c'est l'humain qui les detient toutes, et P03 lit les inventaires de tous les freres. Sur un runner, une seule existe. Le code est identique, le pouvoir ne l'est pas. DEUX PREUVES ONT DIT CE QUI MANQUAIT. P03 est tombee des la separation : elle lit les inventaires voisins, donc il lui faut leurs cles — c'est elle qui a etabli que la liste devait couvrir le voisinage. P16 s'est mise a SAUTER : sa garde ne connaissait que ANSIBLE_VAULT_PASSWORD_FILE. Une preuve sautee se lit trop facilement comme une preuve passee. COUT ASSUME : le chiffre et sa cle cohabiteront sur la meme machine des que les runners recevront la leur. Le pari tient parce que le perimetre est borne. Les six voutes sauvegardees avant rechiffrement. L'ancienne cle maitresse reste sur le poste : la retirer est une decision, pas un nettoyage. make verifier : vert. make prouver : CONFORME, 52 OK, 0 echec, 0 saute — sans ANSIBLE_VAULT_PASSWORD_FILE. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-08-28 14:23:33 -04:00
# UNE VOUTE, UNE CLE — ET ANSIBLE LES TIENT TOUTES A LA FOIS (2026-08-28).
#
# Un seul mot de passe ouvrait les six voutes de la flotte, celle de la fabric comprise.
# Depuis la separation, chaque ecosysteme a la sienne, sous
# `~/.config/setops-vault-<depot-en-minuscules>`.
#
# UN `ANSIBLE_VAULT_PASSWORD_FILE` NE PEUT PLUS SUFFIRE : `cloner_vm_debian.yml` charge
# la voute du TENANT puis celle de l'UNDERLAY dans la meme execution — les identifiants
# Proxmox appartiennent a l'hebergeur, le reste au locataire. `ANSIBLE_VAULT_IDENTITY_LIST`
# porte plusieurs secrets et les essaie tous ; un seul export suffit donc pour les 28
# appels a `ansible-playbook`, sans en toucher un seul.
#
# `?=` : une valeur deja posee dans l'environnement gagne — la GUI et les runners peuvent
# nommer leurs propres cles sans que ce fichier ait a les connaitre.
export ANSIBLE_VAULT_IDENTITY_LIST ?= $(shell python3 scripts/voutes.py identites 2>/dev/null)
# Inventaire de l'instance : un seul par instance dans le modèle « séparation par
# instance ». Détection rétro-compatible : principal > production > lab.
# Surchargeable : make … SETOPS_INVENTAIRE=chemin/hosts.yml
CI : le harnais ne se declenchait que par memoire Trente-huit preuves, des tests, un lint — et RIEN ne les executait sans qu'un humain tape `make`. Le meilleur atout du depot dependait de ne pas oublier. Il a desormais une CI (.forgejo/workflows/verifier.yml) et une cible qui la rejoue a l'identique : `make ci`. CE QUE LA CI A TROUVE AVANT D'EXISTER. Ecrire le workflow supposait de repondre a une question jamais posee : est-ce qu'un depot PUBLIC, seul, se tient ? Mesure sur un clone nu : non, a cinq endroits. - `make instancier` echouait sur le modele public — le tout premier geste du QUICKSTART. Le Makefile forcait `principal/hosts.yml` alors que le modele vit en `production/` ; sa precedence suit desormais celle du code (fichier, puis REPERTOIRE existant, puis defaut). - P32 parcourait les 54 roles sans regarder ce que l'instance deploie. Elle passait sur l'ecosysteme de reference PARCE QU'IL PORTE TOUT. Or les modeles sont des OFFRES : toute offre plus petite que l'ecosysteme complet echouait son propre harnais, pour des services qu'elle ne vend pas. Le perimetre se lit maintenant du plan (groupes de l'inventaire, puis roles composes par leur playbook). - P24 : le modele public ne declarait aucun reseau d'administration — une flotte qu'on construit et ou l'on n'entre plus. `nftables_admin_ssh` est pose, avec le pourquoi. - P33 : verifier_ports.py codait `instance/inventories/principal/hosts.yml` en dur. - P32 et P24 lisaient le symlink `instance/` au lieu de SETOPS_INSTANCE. Toutes de la MEME FAMILLE que P03 avant-hier : une resolution d'inventaire recopiee, une variable d'environnement qui deborde de sa portee. Le depot en compte SEPT ; deux de plus sont corrigees ici, et la septieme le dit en commentaire plutot que de le taire. `make ci` NE TOUCHE AUCUN SYMLINK : le modele public est monte comme instance jetable, vise par SETOPS_INSTANCE/SETOPS_UNDERLAY, detruit en sortant. Deux details mesures parce que devines faux d'abord : l'instance jetable est un DOSSIER FRERE (la federation se decouvre ainsi ; ailleurs, quatre preuves tombent) ; et SETOPS_UNDERLAY n'est pose QUE pour la verification, sinon l'inventaire est ecrit avec une fabric et regenere avec une autre — la commande fabriquait l'ecart qu'elle denonce. RESULTAT : clone nu sans instance ni frere -> 38 OK, 0 echec, 0 saute. Depot de l'exploitant avec ses 3 instances -> 38 OK, 0 echec, 0 saute. Aucun residu. Et le lint du depot a refuse mon propre fichier de CI avant qu'il ne tourne une seule fois (`on:` lu par YAML comme le booleen vrai). Le harnais mordait deja. A AJUSTER AU PREMIER PASSAGE, ecrit en tete du workflow : l'etiquette `runs-on` doit correspondre a un runner Forgejo enregistre, et le runner a besoin du reseau pour pip et ansible-galaxy. Le vert de cette CI dira que le moteur et son modele public se tiennent — pas que la flotte va bien : aucune VM jointe, aucune voute. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 19:29:46 -04:00
#
# LA PRÉCÉDENCE SUIT CELLE DU CODE (`instancier._inventaire`) : fichier existant, puis
# RÉPERTOIRE existant, puis défaut. Le troisième niveau manquait, et comme cette variable
# FORCE la cible, `make instancier` sur une instance neuve visait un `principal/`
# inexistant alors que le plan vivait dans `production/` — l'échec du tout premier geste
# du QUICKSTART, sur le modèle public (mesuré le 2026-08-19). Sans `make`, la même
# commande réussissait : c'est la variable exportée qui décidait, pas le code.
SETOPS_INVENTAIRE ?= $(firstword \
$(wildcard $(SETOPS_INSTANCE)/inventories/principal/hosts.yml) \
$(wildcard $(SETOPS_INSTANCE)/inventories/production/hosts.yml) \
$(patsubst %/,%/hosts.yml,$(wildcard $(SETOPS_INSTANCE)/inventories/principal/)) \
$(patsubst %/,%/hosts.yml,$(wildcard $(SETOPS_INSTANCE)/inventories/production/)) \
$(SETOPS_INSTANCE)/inventories/principal/hosts.yml)
export SETOPS_INVENTAIRE
# Inventaire « modèle » (construction du golden template) : lab > principal > production.
INVENTAIRE_LAB ?= $(firstword $(wildcard $(SETOPS_INSTANCE)/inventories/lab/hosts.yml) $(wildcard $(SETOPS_INSTANCE)/inventories/principal/hosts.yml) $(SETOPS_INSTANCE)/inventories/production/hosts.yml)
INVENTAIRE_PRODUCTION ?= $(SETOPS_INVENTAIRE)
FICHIER_INVENTAIRE ?= $(SETOPS_INVENTAIRE)
FICHIER_DEPENDANCES ?= docs/dependances-groupes.yml
inseminer : le geste sort de mes mains et entre dans le depot L insemination avait ete conduite A LA MAIN depuis le runner du site — hors du depot, donc sans preuve. Elle a maintenant sa cible : make inseminer TENANT=OPS-Chezlepro TENANT= PLUTOT QUE LE SYMLINK instance : le runner du SITE amorce PLUSIEURS locataires ; pointer un lien global sur l un d eux le ferait se prendre pour ce tenant. Il en NOMME un par commande. Ca borne aussi le couplage que creer-vm imposait en silence — rien ne disait sur quels tenants ce lien pouvait pointer. L HOTE SE DERIVE : celui qui porte serveur_ops_tenant. Meme critere que le flux d insemination et que la cle SSH du runner — le meme mot borne les trois pouvoirs. P54 GARDE LA LIGNE DE PARTAGE la ou elle glisserait sans bruit. Les deux couches retenues sont les seules qui ne reclament aucun secret. Le jour ou l on en ajouterait une, le deploiement echouerait chez le tenant sur une valeur vide, et ce message ne dirait pas qu un POUVOIR a ete franchi. Controle negatif : ajouter client_pki, la couche suivante, fait echouer la preuve. UN GARDE-FOU EXISTANT A INTERCEPTE UNE INSEMINATION MAL DIRIGEE. Le make parent exporte SETOPS_INVENTAIRE ; ma resolution en heritait et visait l inventaire d un AUTRE ecosysteme. Le refus vient d inventory_rules, pas de la cible — exactement l erreur qu un runner servant plusieurs locataires commettrait en silence. make verifier : vert. make prouver : CONFORME, 54 OK, 0 echec, 0 saute. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-08-28 17:14:37 -04:00
# LES COUCHES DE L'INSEMINATION — CE QUE LE SITE POSE CHEZ UN TENANT, ET RIEN DE PLUS.
#
# Un ecosysteme neuf ne s'amorce pas lui-meme : quelqu'un doit poser sa premiere machine
# et lui donner de quoi continuer. Le runner du SITE le fait — socle, moteur, plan,
# plancher de resolution — puis s'arrete.
#
# CES DEUX-LA ET AUCUNE AUTRE, parce qu'elles ne demandent AUCUN SECRET. Le site ne
# detient pas la voute d'un tenant, et ne doit jamais la detenir : ce qui vient apres
# (`client_pki`, `serveur_ops_tenant`...) reclame des valeurs qu'il n'a pas. La liste
# n'est donc pas une commodite, c'est la LIGNE DE PARTAGE DU POUVOIR, et la preuve P54
# refuse toute couche qui y ferait entrer un secret.
COUCHES_INSEMINATION ?= serveur_debian serveur_ops
GROUPE_MODELE ?= modeles_vm
GROUPE_DEBIAN ?= serveur_debian
GROUPE_HOTES_ACTIFS ?= hotes_actifs
LIMITE ?= $(GROUPE_DEBIAN)
HOTE ?=
ADRESSE_IP ?=
GROUPES ?= $(GROUPE_DEBIAN)
GROUPE ?= $(GROUPE_DEBIAN)
UTILISATEUR_ANSIBLE ?= ansible
gabarit : refabrique, minimal, et puise aux ressources du SITE REFABRIQUE (VMID 9006, modeleSetOPS-minimal). Quatre roles au lieu de dix-sept : qemu_guest_agent, cloud_init, sudo_ansible, ssh_baseline — des conditions d existence, pas des choix d efficacite. Tout le reste vient du socle, et P56 refuse qu un role retire ne soit repris par personne. FABRIQUE CHEZ LE SITE, ET C EST LA DECISION QUI COMPTE. « Les ressources du SITE font autorite pour tous ses artefacts ; elles servent les tenants jusqu a ce qu ils s emancipent. » Il se fabriquait DEHORS : `-i "<ip>,"` ne porte aucun group_vars, donc ni mandataire ni resolveur. L ancien gabarit allait chercher ses paquets chez Debian et resolvait chez l ancien LAN — 192.168.10.10, lu sur la VM 99998. Le site avait son cache et son resolveur, et son propre artefact les ignorait. Desormais la fabrication DERIVE ses ressources du plan du site (underlay --adresses) et tourne sur le reseau du genome. PROUVE : dix requetes de 10.0.33.31 servies par site-cache-01, resolveur pose a 10.0.34.11. L IDENTITE DU GABARIT VIENT DU SITE, PLUS DU TENANT. `proxmox_clone_vmid_modele` vivait dans les group_vars de l ecosysteme : deux tenants pouvaient cloner deux gabarits differents sans que rien ne le dise, et un tenant decidait d un objet dont descend chaque VM de chaque ecosysteme. Meme mouvement que l INDEX (2026-08-25) : le site ALLOUE, le tenant RECOIT. Cle `gabarit` du plan du site, lue par underlay --gabarit. PIEGE FERME EN CHEMIN : creer-vm passait VMID_MODELE="$SETOPS_VMID_MODELE" a cloner-vm, or inventory_host.py n emet PAS cette variable. Elle valait donc le VIDE, et ce vide ECRASAIT la valeur derivee — le gabarit du tenant reprenait la main sans bruit. ET UN PIEGE DEJA DOCUMENTE, PAYE UNE TROISIEME FOIS : le chemin du controleur contient une espace, et `lookup('pipe', ...)` le decoupait. Guillemets. EPROUVE DE BOUT EN BOUT : VM clonee du gabarit minimal — nom, adresse, machine-id neuf, cle d hote regeneree, agent invite actif ; puis socle applique dessus, changed=5, 0 echec, auditd compris. VM d essai retiree. make verifier : vert. make prouver : CONFORME, 56 OK, 0 echec. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-09-01 13:46:31 -04:00
# LE GABARIT VIENT DU SITE, PAS DU TENANT (2026-09-01).
#
# « Les ressources du SITE font autorite pour tous ses artefacts. » Le gabarit en est un :
# c'est de lui que descend chaque VM de chaque tenant. Son VMID vivait pourtant dans les
# group_vars de l'ecosysteme — deux tenants pouvaient donc cloner deux gabarits differents
# sans que rien ne le dise, et un tenant decidait d'un objet qui n'est pas a lui.
#
# Meme mouvement que l'INDEX, tranche le 2026-08-25 : le site ALLOUE, le tenant RECOIT.
#
# `?=` : une valeur donnee en ligne de commande gagne — necessaire pour eprouver un
# gabarit neuf avant de le declarer. Et si le site n'en declare aucun, la valeur reste
# vide et le plan du tenant reprend la main : degrader, jamais deviner.
VMID_MODELE ?= $(shell python3 scripts/underlay.py --gabarit 2>/dev/null | sed -n 's/^vmid=//p')
# NOTE SUR LE PIEGE QU'ON VIENT DE FERMER : `creer-vm` passait
# `VMID_MODELE="$$SETOPS_VMID_MODELE"` a `cloner-vm`, or `inventory_host.py` n'emet PAS
# cette variable — elle valait donc le VIDE, et ce vide ECRASAIT la valeur derivee. Le
# gabarit du tenant reprenait la main sans que rien ne le dise. `:-` rend la derivation au
# site quand l'inventaire ne dit rien.
VMID ?=
NOEUD_PROXMOX ?=
D-86 mis a l epreuve : Chezlepro rasee et refaite depuis zero Quatorze machines detruites, quatorze refaites. 1 h 08, 0 injoignable. D-86 TIENT, et la mesure le dit : 30 resultats de controle recus a 12:20:41 alors que la reconstruction ne s est terminee qu a 12:35:29. Trente services rapportaient leur etat pendant que Keycloak, Forgejo et Nextcloud montaient encore. Sa limite, dite franchement : D-86 affirme que le DNS peut rester en couche 6 grace au plancher /etc/hosts. Or _amorcer-socle monte infra-dns-01 EN ENTIER d abord. Le DNS etait debout, le plancher n a rien eu a porter, et la partie la plus audacieuse de la decision reste non testee. LE PLACEMENT DES VM N ETAIT DECLARE NULLE PART. Les quatorze vivaient sur asgard depuis toujours, mais SETOPS_NOEUD valait le vide : ce placement n existait que dans l etat d execution de Proxmox. Sans cible, le clone reste sur le noeud du gabarit - vishnu, qui a 14 Go libres et heberge eregion et site-forge-01. Le defaut avait survecu a la reconstruction du 2026-09-02 parce que les VM existaient deja et que le clone les sautait. Il faut un from-zero VRAI pour voir ce genre de chose. LE CLONAGE ETAIT 45 FOIS TROP LENT, et pas a cause du reseau : source et destination etaient sur le meme stockage. C est le LVM epais qui interdit a Proxmox tout clone autre que complet. Gabarit deplace sur CephNVMe, mesure 480 s -> 8 s. On garde les clones complets : le clone lie descend a 1 s mais enchaine chaque VM a l image de base pour toujours. Un champ stockage: entre au gabarit, branche en quatre points - declare, expose, consomme par le Makefile, garde par gabarit_etat. Declarer sans consommer, c est decorer. ICINGA NE POUVAIT PAS FAIRE SES PROPRES CONTROLES : icinga2 n apporte aucun greffon, et la supervision etant passive, le manque etait masque. Quatorze ping4 muets d une seule cause. monitoring-plugins-basic entre au role. Le site l avait deja - pose A LA MAIN, jamais declare : troisieme occurrence du jour d un etat qui ne survivrait pas a une reconstruction. Trois defauts laisses ouverts : le cache apt attendu pendant l amorcage, la regression D-85 sur /etc/cloud - qui a fait decrocher deux hotes de la tache deposant icinga-ca.crt, rendant leurs sondes muettes en silence - et auditd sans regles dans le gabarit. Mesure finale : Icinga 93 OK sur 98, site 51/51, Prometheus 15/15, prouver 64 OK, lint 0 defaut. Les quatre non-OK restants sont des sauvegarde dont le minuteur nocturne n a pas encore visite une flotte nee il y a deux heures. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-10 13:06:45 -04:00
# LE STOCKAGE DU CLONE SUIT CELUI DU GABARIT, ET IL EST DECLARE (2026-09-10).
#
# Vide, `qm clone` heritait SILENCIEUSEMENT du stockage de la source. Ca marchait, et
# c'est bien la le probleme : quand le gabarit a change de stockage, la destination a
# suivi sans qu'aucun fichier ne le dise. Le site le declare desormais
# (`gabarit.stockage`), et cette ligne le rend a la commande de clonage.
#
# `SETOPS_STOCKAGE` — le `stockage:` d'une machine au plan du tenant — garde la priorite
# (voir `creer-vm`) : un ecosysteme peut poser une machine ailleurs, ce qui n'est pas la
# meme question que « d'ou nait la flotte ».
STOCKAGE_PROXMOX ?= $(shell python3 scripts/underlay.py --gabarit 2>/dev/null | sed -n 's/^stockage=//p')
FORMAT_DISQUE ?=
TAILLE_DISQUE ?=
DISQUE_PROXMOX ?=
CIDR ?= 24
PASSERELLE ?=
DNS ?=
DHCP ?= false
CIUSER ?=
CLE_SSH_PUBLIQUE ?=
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 VOYAGE PAR L'ENVIRONNEMENT, ET ARRIVE EN JSON. MESURE (2026-08-28).
#
# Une cle publique contient des ESPACES — type, materiel, commentaire — et deux couches
# les mangent en silence :
#
# `ansible-playbook -e cle=valeur` decoupe la chaine AU SHLEX : tout ce qui suit le
# premier espace devient d'autres paires cle=valeur. Le module recevait
# `"sshkeys": "ssh-ed25519"` — le premier mot, rien d'autre.
#
# Et une variable `make` passee a un sous-make est un chemin tout aussi fragile pour
# une valeur a espaces.
#
# Proxmox repondait `500 SSH public key validation error` : un message qui ne parle ni
# de shlex, ni de make, ni d'espaces. Le contrôle qui l'a isole : rejouer `cloner-vm`
# SANS la cle — la tache passe.
#
# D'ou la forme retenue : l'environnement (`SETOPS_CLES_AMORCAGE`, exporte par
# `creer-vm`) pour le transport, et `-e '{"...": "..."}'` en JSON pour l'entree dans
# Ansible — le seul mode de `-e` qui ne decoupe rien.
PONT_PROXMOX ?=
VLAN ?=
DEMARRER ?=
CLONE_COMPLET ?=
CONFIRMER ?= false
VERIFICATION ?= false
DIFF ?= false
ETIQUETTES ?=
SAUTER_ETIQUETTES ?=
VARIABLES ?=
OPTIONS_PLAYBOOK :=
ifneq ($(LIMITE),)
OPTIONS_PLAYBOOK += --limit $(LIMITE)
endif
ifeq ($(VERIFICATION),true)
OPTIONS_PLAYBOOK += --check
endif
ifeq ($(DIFF),true)
OPTIONS_PLAYBOOK += --diff
endif
ifneq ($(ETIQUETTES),)
OPTIONS_PLAYBOOK += --tags $(ETIQUETTES)
endif
ifneq ($(SAUTER_ETIQUETTES),)
OPTIONS_PLAYBOOK += --skip-tags $(SAUTER_ETIQUETTES)
endif
ifneq ($(VARIABLES),)
OPTIONS_PLAYBOOK += -e $(VARIABLES)
endif
PLAYBOOK_PREPARER_MODELE := playbooks/modeles_vm/debian13_proxmox_preparer.yml
PLAYBOOK_VERIFIER_MODELE := playbooks/modeles_vm/debian13_proxmox_verifier.yml
PLAYBOOK_NETTOYER_MODELE := playbooks/modeles_vm/debian13_proxmox_nettoyer.yml
PLAYBOOK_VERIFIER_HOTE := playbooks/maintenance/verifier_hote_debian.yml
PLAYBOOK_PROXMOX_CLONER_VM := playbooks/proxmox/cloner_vm_debian.yml
DOSSIER_PLAYBOOKS_GROUPES := playbooks/groupes
.DEFAULT_GOAL := aide
.PHONY: ansible-runtime
ansible-runtime: ## Prepare le repertoire temporaire local d'Ansible (prerequis interne des cibles qui deploient)
@mkdir -p "$(ANSIBLE_LOCAL_TEMP)"
@mkdir -p "$(ANSIBLE_SSH_CONTROL_PATH_DIR)"
.PHONY: _instance-requise
_instance-requise:
@if [[ ! -f "$(SETOPS_INSTANCE)/plan/serveurs.yml" ]]; then \
printf '%s\n' "Aucune instance configuree : '$(SETOPS_INSTANCE)/plan' introuvable."; \
printf '%s\n' "Demarre avec QUICKSTART.md. En bref :"; \
printf '%s\n' " cp -r exemples/modeles/<modele> ../mon-instance && ln -s ../mon-instance instance"; \
printf '%s\n' " modeles disponibles : $$(ls exemples/modeles 2>/dev/null | grep -v '\.md' | tr '\n' ' ')"; \
exit 2; \
fi
.PHONY: aide
aide: ## Affiche l'aide detaillee du moteur (au-dela de cette liste)
@printf '%s\n' 'Set-OPS — moteur d ecosystemes numeriques souverains'
@printf '%s\n' ''
@printf '%s\n' 'Nouveau ? -> QUICKSTART.md (de zero a ton ecosysteme sur Proxmox)'
@printf '%s\n' 'Flux: editer le plan -> make instancier -> make instancier-appliquer -> make deployer'
@printf '%s\n' ''
@printf '%s\n' 'VM'
@printf '%s\n' ' Creer une VM (VMID/IP/VLAN/passerelle lus dans le plan):'
@printf '%s\n' ' make creer-vm HOTE=web-frontal-01'
@printf '%s\n' ' Cloner seulement, sans passer par le plan:'
@printf '%s\n' ' make cloner-vm HOTE=web-frontal-01 VMID=95301 VLAN=15 ADRESSE_IP=10.0.2.31 PASSERELLE=10.0.2.1'
@printf '%s\n' ' Configurer Proxmox et le Vault API (parametres: docs/config-proxmox.md):'
@printf '%s\n' ' make config'
@printf '%s\n' ''
@printf '%s\n' 'Hotes'
@printf '%s\n' ' Planifier/modifier un hote: editer le plan, puis regenerer:'
@printf '%s\n' ' editer instance/plan/serveurs.yml (ou la vue Serveurs du GUI)'
@printf '%s\n' ' make instancier-appliquer'
@printf '%s\n' ' Verifier un deploiement a blanc (dry-run):'
@printf '%s\n' ' make verifier-deploiement HOTE=web-frontal-01'
@printf '%s\n' ' Remettre un hote en conformite selon ses groupes:'
@printf '%s\n' ' make deployer HOTE=web-frontal-01'
@printf '%s\n' ' Afficher un hote:'
@printf '%s\n' ' make hote-afficher HOTE=web-frontal-01'
@printf '%s\n' ' Diagnostiquer:'
@printf '%s\n' ' make verifier-hote LIMITE=web-frontal-01'
@printf '%s\n' ''
@printf '%s\n' 'Groupes'
@printf '%s\n' ' Appliquer un groupe complet:'
@printf '%s\n' ' make deployer-groupe GROUPE=serveur_debian'
@printf '%s\n' ' Convention:'
@printf '%s\n' ' groupe serveur_debian -> playbooks/groupes/serveur_debian.yml'
@printf '%s\n' ''
@printf '%s\n' 'Ecosysteme complet (orchestrateur)'
@printf '%s\n' ' Ordre de deploiement (couches + graphe):'
@printf '%s\n' ' make site-verifier # valide la coherence couches/graphe'
@printf '%s\n' ' python3 scripts/orchestrer.py ordre'
@printf '%s\n' ' (Re)generer playbooks/site.yml ordonne:'
@printf '%s\n' ' make site'
@printf '%s\n' ' CONFIGURER la flotte existante, couche par couche (2b, VM deja creees):'
@printf '%s\n' ' make deployer-tout CONFIRMER=true # (MODE_CHECK=1 pour un essai a blanc idempotent)'
@printf '%s\n' ' CREER toutes les VM du plan (2a, clone Proxmox):'
@printf '%s\n' ' make flotte-creer CONFIRMER=true'
@printf '%s\n' ' RECONSTRUIRE from-zero = creer les VM PUIS deployer (2a+2b, VM inexistantes):'
@printf '%s\n' ' make reconstruire CONFIRMER=true # alias: make myDay CONFIRMER=true'
@printf '%s\n' ''
@printf '%s\n' 'Flux reseau (pare-feu / audit)'
@printf '%s\n' ' Matrice d audit + apercus nftables resolus (NON actives):'
@printf '%s\n' ' make flux # -> docs/registre-flux.md + instance/flux-genere/*.nft'
@printf '%s\n' ' make flux-verifier # valide schema + coherence de matrice'
@printf '%s\n' ''
@printf '%s\n' 'Wiki pedagogique'
@printf '%s\n' ' Publier wiki/ dans le wiki Forgejo (source versionnee -> vue browsable):'
@printf '%s\n' ' make wiki-publier WIKI_REMOTE=https://forge.<domaine>/<proprio>/<depot>.wiki.git'
@printf '%s\n' ''
@printf '%s\n' 'Inventaires'
@printf '%s\n' ' Graphe de production:'
@printf '%s\n' ' make inventaire'
@printf '%s\n' ' Graphe explicite:'
documentation : la tournee des 74 documents, parce qu un balayage ne lit pas La revision a commence par un balayage par motifs — chemins morts, cibles make absentes, comptes derives. Il a trouve une trentaine d ecarts et rate presque tout le reste : un motif ne voit que ce qui s exprime en motif. make hote-planifier en est l exemple. La cible EXISTE, donc le controle passait au vert. C est une cible depreciee qui refuse et sort en 2, recommandee par AGENTS.md, et qui contredit la REGLE D OR du meme fichier trois ecrans plus haut. Il fallait lire pour la voir. 74 documents lus un par un. 66 corriges, 8 exacts. CE QUI ETAIT FRANCHEMENT FAUX AGENTS.md, la source d autorite, annoncait la flotte pas encore executee contre des VM reelles. Elle a ete rasee et remontee depuis zero trois fois. ecosysteme-chezlepro.md, le document montre a un client, portait la meme phrase : il se sous-vendait gravement. courriel-conception.md s ouvrait sur aucun role n est encore ecrit, au-dessus de son propre paragraphe 1 qui les nomme. autorisation.md se terminait sur rien n est construit alors qu il rapporte des mesures datees du role en fonctionnement. hebergeur-exploitation.md disait rien n est fait d un depot qui existe. filiation-emancipation.md se contredisait a deux ecrans de distance. DES MODELES DECRITS D APRES UN MONDE ANTERIEUR Le resolveur : cinq documents decrivaient un Unbound par VM en opt-in, trois le donnaient en exemple d integration FACULTATIVE — il est universel depuis le 2026-08-24. L adressage de nomenclature-vm.md : reseau unique, VLAN 11-15, VMID a cinq chiffres. Le nommage SDN de sdn-evpn.md contre le code : c est le wiki qui avait raison. CE QUI CASSE AU PREMIER ESSAI Le nom du gabarit dore etait faux a quatre endroits, dont la procedure qui le FABRIQUE et le critere R2 de l epreuve d operateur independant. preparer-un-site-hebergeur.md avertissait qu une VM faite a la main serait detruite : raser derive du plan, il ne la detruira jamais — le risque est l inverse. Un mot de passe d essai en clair dans un depot public. DEUX PREUVES ETENDUES, ET UNE QUI SE TROMPAIT ELLE-MEME P57 couvre les groupes : elle a signale aussitot 29 groupes annonces au-dessus d un tableau qui en cite 40. P29 confronte le tableau de authentification.md aux declarations reelles : 12 annonces, 21 reels. Et P57 imposait un chiffre faux — 56 preuves alors que le depot en porte 57, la conditionnelle vivant hors de tout comptage. Un garde-fou qui fait respecter une erreur ajoute l assurance a l erreur. CE QUI RESTE, ET QU AUCUNE PREUVE NE TIENT Deux comptes trouves a la main. Et une lacune reelle : rien ne garde les meta/acces.yml — ni qu un service web-sso en porte un, ni que le groupe qu il nomme existe. P29 tient les positions d authentification, personne ne tient les habilitations. make prouver : CONFORME, 56 OK, 0 echec, 1 saute. 0 lien mort. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-06 16:18:23 -04:00
@printf '%s\n' ' make inventaire-graphe FICHIER_INVENTAIRE=$(SETOPS_INVENTAIRE)'
@printf '%s\n' ' Verifier les inventaires:'
@printf '%s\n' ' make inventaire-verifier'
@printf '%s\n' ' Lister les donnees brutes:'
@printf '%s\n' ' make inventaire-lister'
@printf '%s\n' ' Interface locale de gestion:'
@printf '%s\n' ' make inventaire-ui'
@printf '%s\n' ''
@printf '%s\n' 'Modele Debian 13 Proxmox'
@printf '%s\n' ' Construire et verifier:'
@printf '%s\n' ' make preparer-modele'
@printf '%s\n' ' make verifier-modele'
@printf '%s\n' ' Nettoyage final protege:'
@printf '%s\n' ' make nettoyer-modele CONFIRMER=true'
@printf '%s\n' ''
@printf '%s\n' 'Validation'
@printf '%s\n' ' make syntaxe'
@printf '%s\n' ' make lint'
@printf '%s\n' ' make verifier'
@printf '%s\n' ''
@printf '%s\n' 'Variables frequentes'
@printf '%s\n' ' HOTE=web-frontal-01 GROUPE=serveur_debian GROUPES="serveur_debian serveur_durci"'
@printf '%s\n' ' VMID=95301 VLAN=15 ADRESSE_IP=10.0.2.31 PASSERELLE=10.0.2.1'
documentation : la tournee des 74 documents, parce qu un balayage ne lit pas La revision a commence par un balayage par motifs — chemins morts, cibles make absentes, comptes derives. Il a trouve une trentaine d ecarts et rate presque tout le reste : un motif ne voit que ce qui s exprime en motif. make hote-planifier en est l exemple. La cible EXISTE, donc le controle passait au vert. C est une cible depreciee qui refuse et sort en 2, recommandee par AGENTS.md, et qui contredit la REGLE D OR du meme fichier trois ecrans plus haut. Il fallait lire pour la voir. 74 documents lus un par un. 66 corriges, 8 exacts. CE QUI ETAIT FRANCHEMENT FAUX AGENTS.md, la source d autorite, annoncait la flotte pas encore executee contre des VM reelles. Elle a ete rasee et remontee depuis zero trois fois. ecosysteme-chezlepro.md, le document montre a un client, portait la meme phrase : il se sous-vendait gravement. courriel-conception.md s ouvrait sur aucun role n est encore ecrit, au-dessus de son propre paragraphe 1 qui les nomme. autorisation.md se terminait sur rien n est construit alors qu il rapporte des mesures datees du role en fonctionnement. hebergeur-exploitation.md disait rien n est fait d un depot qui existe. filiation-emancipation.md se contredisait a deux ecrans de distance. DES MODELES DECRITS D APRES UN MONDE ANTERIEUR Le resolveur : cinq documents decrivaient un Unbound par VM en opt-in, trois le donnaient en exemple d integration FACULTATIVE — il est universel depuis le 2026-08-24. L adressage de nomenclature-vm.md : reseau unique, VLAN 11-15, VMID a cinq chiffres. Le nommage SDN de sdn-evpn.md contre le code : c est le wiki qui avait raison. CE QUI CASSE AU PREMIER ESSAI Le nom du gabarit dore etait faux a quatre endroits, dont la procedure qui le FABRIQUE et le critere R2 de l epreuve d operateur independant. preparer-un-site-hebergeur.md avertissait qu une VM faite a la main serait detruite : raser derive du plan, il ne la detruira jamais — le risque est l inverse. Un mot de passe d essai en clair dans un depot public. DEUX PREUVES ETENDUES, ET UNE QUI SE TROMPAIT ELLE-MEME P57 couvre les groupes : elle a signale aussitot 29 groupes annonces au-dessus d un tableau qui en cite 40. P29 confronte le tableau de authentification.md aux declarations reelles : 12 annonces, 21 reels. Et P57 imposait un chiffre faux — 56 preuves alors que le depot en porte 57, la conditionnelle vivant hors de tout comptage. Un garde-fou qui fait respecter une erreur ajoute l assurance a l erreur. CE QUI RESTE, ET QU AUCUNE PREUVE NE TIENT Deux comptes trouves a la main. Et une lacune reelle : rien ne garde les meta/acces.yml — ni qu un service web-sso en porte un, ni que le groupe qu il nomme existe. P29 tient les positions d authentification, personne ne tient les habilitations. make prouver : CONFORME, 56 OK, 0 echec, 1 saute. 0 lien mort. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-06 16:18:23 -04:00
@printf '%s\n' ' FICHIER_INVENTAIRE=$(SETOPS_INVENTAIRE) FICHIER_DEPENDANCES=docs/dependances-groupes.yml CONFIRMER=true'
.PHONY: lint
lint: ansible-runtime ## Passe ansible-lint sur tout le depot
ansible-lint
.PHONY: syntaxe syntaxe-modele syntaxe-nettoyage syntaxe-verification-modele syntaxe-verification-hote syntaxe-groupes syntaxe-proxmox
syntaxe: syntaxe-modele syntaxe-verification-modele syntaxe-nettoyage syntaxe-verification-hote syntaxe-groupes syntaxe-proxmox ## Verifie la syntaxe de TOUS les playbooks (modele, hote, groupes, proxmox)
syntaxe-modele: ansible-runtime ## Verifie la syntaxe du playbook de preparation du gabarit dore
ansible-playbook -i $(INVENTAIRE_MODELE) $(UTILISATEUR_MODELE) $(PLAYBOOK_PREPARER_MODELE) --syntax-check
syntaxe-verification-modele: ansible-runtime ## Verifie la syntaxe du playbook de verification du gabarit
ansible-playbook -i $(INVENTAIRE_MODELE) $(UTILISATEUR_MODELE) $(PLAYBOOK_VERIFIER_MODELE) --syntax-check
syntaxe-nettoyage: ansible-runtime ## Verifie la syntaxe du playbook de nettoyage du gabarit
ansible-playbook -i $(INVENTAIRE_MODELE) $(PLAYBOOK_NETTOYER_MODELE) --syntax-check
syntaxe-verification-hote: ansible-runtime ## Verifie la syntaxe du playbook de verification d'hote
ansible-playbook -i $(INVENTAIRE_PRODUCTION) $(PLAYBOOK_VERIFIER_HOTE) --syntax-check
documentation : la tournee des 74 documents, parce qu un balayage ne lit pas La revision a commence par un balayage par motifs — chemins morts, cibles make absentes, comptes derives. Il a trouve une trentaine d ecarts et rate presque tout le reste : un motif ne voit que ce qui s exprime en motif. make hote-planifier en est l exemple. La cible EXISTE, donc le controle passait au vert. C est une cible depreciee qui refuse et sort en 2, recommandee par AGENTS.md, et qui contredit la REGLE D OR du meme fichier trois ecrans plus haut. Il fallait lire pour la voir. 74 documents lus un par un. 66 corriges, 8 exacts. CE QUI ETAIT FRANCHEMENT FAUX AGENTS.md, la source d autorite, annoncait la flotte pas encore executee contre des VM reelles. Elle a ete rasee et remontee depuis zero trois fois. ecosysteme-chezlepro.md, le document montre a un client, portait la meme phrase : il se sous-vendait gravement. courriel-conception.md s ouvrait sur aucun role n est encore ecrit, au-dessus de son propre paragraphe 1 qui les nomme. autorisation.md se terminait sur rien n est construit alors qu il rapporte des mesures datees du role en fonctionnement. hebergeur-exploitation.md disait rien n est fait d un depot qui existe. filiation-emancipation.md se contredisait a deux ecrans de distance. DES MODELES DECRITS D APRES UN MONDE ANTERIEUR Le resolveur : cinq documents decrivaient un Unbound par VM en opt-in, trois le donnaient en exemple d integration FACULTATIVE — il est universel depuis le 2026-08-24. L adressage de nomenclature-vm.md : reseau unique, VLAN 11-15, VMID a cinq chiffres. Le nommage SDN de sdn-evpn.md contre le code : c est le wiki qui avait raison. CE QUI CASSE AU PREMIER ESSAI Le nom du gabarit dore etait faux a quatre endroits, dont la procedure qui le FABRIQUE et le critere R2 de l epreuve d operateur independant. preparer-un-site-hebergeur.md avertissait qu une VM faite a la main serait detruite : raser derive du plan, il ne la detruira jamais — le risque est l inverse. Un mot de passe d essai en clair dans un depot public. DEUX PREUVES ETENDUES, ET UNE QUI SE TROMPAIT ELLE-MEME P57 couvre les groupes : elle a signale aussitot 29 groupes annonces au-dessus d un tableau qui en cite 40. P29 confronte le tableau de authentification.md aux declarations reelles : 12 annonces, 21 reels. Et P57 imposait un chiffre faux — 56 preuves alors que le depot en porte 57, la conditionnelle vivant hors de tout comptage. Un garde-fou qui fait respecter une erreur ajoute l assurance a l erreur. CE QUI RESTE, ET QU AUCUNE PREUVE NE TIENT Deux comptes trouves a la main. Et une lacune reelle : rien ne garde les meta/acces.yml — ni qu un service web-sso en porte un, ni que le groupe qu il nomme existe. P29 tient les positions d authentification, personne ne tient les habilitations. make prouver : CONFORME, 56 OK, 0 echec, 1 saute. 0 lien mort. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-06 16:18:23 -04:00
syntaxe-groupes: ansible-runtime ## Verifie la syntaxe de TOUS les playbooks de groupe (playbooks/groupes/*.yml)
@for playbook in $(DOSSIER_PLAYBOOKS_GROUPES)/*.yml; do \
ansible-playbook -i $(INVENTAIRE_PRODUCTION) "$$playbook" --syntax-check; \
done
syntaxe-proxmox: ansible-runtime ## Verifie la syntaxe du playbook de clonage de VM
ansible-playbook -i localhost, $(PLAYBOOK_PROXMOX_CLONER_VM) --syntax-check
.PHONY: test
test: ## Lance les tests unitaires (derivation de nomenclature et d'inventaire)
python3 scripts/tests/test_inventory_host.py
python3 scripts/tests/test_raser.py
raser : lire le RESULTAT de la destruction, pas son accuse de reception Le defaut note hier est corrige. `raser` concluait au succes sur la reponse immediate de l'API : le DELETE rend un UPID et la main tout de suite, la destruction se fait en tache de fond, et elle peut echouer APRES. Le 2026-08-10, six VM ont ete rapportees « detruites » alors qu'elles etaient toujours la — la tache sortait sur « VM is locked (clone) », verrou laisse par des clonages interrompus. Confondre « demande acceptee » et « travail fait » est le pire mensonge possible pour la SEULE commande destructive du moteur : on croit la place libre, on relance la construction, et rien ne se cree sans qu'on comprenne pourquoi. _attendre_tache() relit l'UPID, interroge l'etat jusqu'a `stopped` et rend l'exitstatus reel. Chaque VM est annoncee detruite OU en echec, avec la cause telle que le cluster l'a donnee ; le compte final ne ment plus. test_raser_resultat.py fabrique la situation exacte : un faux cluster qui accepte tout puis rend une tache terminee en erreur. EPROUVE DANS LES DEUX SENS — avec l'ancien comportement retabli temporairement il echoue en designant le defaut, avec le correctif il passe. Raccorde a `make test`, donc rejoue par P02. Meme motif que le clonage corrige une heure plus tot, dans l'autre sens : une operation asynchrone dont on ne verifie pas l'issue. Les deux venaient du passage a des appels d'API directs, ou plus rien n'attend a notre place. Verifie : make test vert, prouver.py 35 OK. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 22:47:28 -04:00
python3 scripts/tests/test_raser_resultat.py
adressage : l'index est borne [0-255] — 10.300.0.0/16 n'est pas un reseau Rien ne bornait `index`. supernet_de(300) rendait « 10.300.0.0/16 » : une CHAINE qui ressemble a un reseau. Elle traverse tout le moteur sans bruit et n'echoue qu'au premier ip_network() qui la lit, tres loin de l'index fautif. La borne est posee A LA SOURCE (valider_index), pas dans un validateur de plan : supernet_de, base3_de et vlan_de y passent toutes, donc aucune ne peut fabriquer une adresse invalide — d'ou qu'on l'appelle : plan, GUI, devis ou test. Elle protege un SECOND plafond, moins visible : a l'index 255 le VLAN vaut 3550+zone, sous les 4094 du 802.1Q. Un index a trois chiffres debordait aussi la. Garde statique ajoutee au controle de federation (P21) : elle nomme le depot fautif au lieu de laisser l'erreur remonter d'une bibliotheque. LE TEST A TROUVE CE QUE LA RELECTURE N'AVAIT PAS VU : valider_index n'attrapait que TypeError et ValueError, or int(float('inf')) leve OverflowError — un infini faisait PLANTER la garde au lieu d'etre refuse. test_underlay_bande_basse.py -> test_adressage_derive.py (il ne parlait plus seulement de la bande basse). 12 cas, dont les refus. Piege de structure au passage : les nouveaux cas, ajoutes apres le bloc `if __name__ == "__main__"`, ne s'executaient pas — le bloc tourne avant que les fonctions suivantes ne soient definies, et le compte affichait « 7 tests » au lieu de 12. Un harnais qui compte ses propres tests doit etre lu. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 16:21:33 -04:00
python3 scripts/tests/test_adressage_derive.py
GUI : le panneau d'intrants effacait la memoire ecrite du depot Un enregistrement du panneau « Intrants de base », a 13:48, a emporte 121 lignes d'explications dans quatre fichiers — dont celle qui disait POURQUOI la valeur qu'on venait de changer avait ete choisie (le plan de gestion reste en 10.0.0.0/24 tant que la frontiere ne sait pas classer un second CIDR, D-61). Ces phrases sont la seule trace de raisonnements qu'aucun code ne redit. `safe_dump` les effacait toutes a chaque sauvegarde, en retriant les clefs au passage. LE DEPOT CONNAISSAIT DEJA LE GESTE JUSTE. `_ecrire_intrants_fabric` (underlay.yml) et `_ecrire_index_nomenclature` remplacent LA LIGNE sans toucher au reste ; leur commentaire dit meme « un safe_dump les effacerait toutes ». Les quatre fichiers d'intrants n'avaient jamais recu ce traitement. Ce qui manquait pour l'etendre : savoir remplacer une valeur de LISTE, qui tient sur plusieurs lignes. `_fusion_chirurgicale`, trois regles : - une clef dont la valeur ne change pas n'est PAS reecrite (zero bruit au diff) ; - les commentaires internes a un bloc remplace sont conserves, jamais juges ; - une clef absente du fichier est ajoutee a la fin, jamais inseree au hasard. La deuxieme est assumee : une explication devenue fausse survit a la valeur qu'elle explique. Corriger une phrase est un geste humain ; l'effacer parce qu'un champ a bouge, non. Meme principe que la fusion des clefs posee le 2026-08-10. EPROUVE SUR LE FICHIER REEL, pas sur un exemple : l'enregistrement de 13:48 rejoue sur la version d'avant (tiree de git) rend 41 lignes -> 41, 30 commentaires -> 30, 0 perdu, et un diff d'UNE ligne. Neuf tests dans scripts/tests/test_gui_intrants.py, branches sur `make test`. DEUX FOIS LE MEME GESTE DESTRUCTEUR, SUR LE MEME CHEMIN : le 10 aout ce panneau perdait des CLEFS (dns_amorcage, amorcage_acces_courriel — une VM qui nait sans resolution), le 18 des COMMENTAIRES. La premiere fois avait valu une fusion, pas un test. C'est le test qui manquait. Non touche, et dit comme tel : les registres (bases, applications, serveurs, domaines) passent toujours par safe_dump. Ils sont structures et n'ont pas perdu de commentaires au meme enregistrement — a reprendre si l'un d'eux en porte un jour. make test 9/9 sur le nouveau fichier ; prouver 37 OK, 0 echec, 0 saute. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 15:05:12 -04:00
python3 scripts/tests/test_gui_intrants.py
devis de placement : mourir n'est pas un diagnostic Soir de reconstruction, VPN pas encore monte. `make placement-plan` — le devis qu'on lance AVANT quarante minutes de deploiement — rendait `AttributeError: 'str' object has no attribute 'get'` : dix lignes de trace Python pour dire « le nom `asgard` ne se resout pas d'ici ». En panne, `Cluster.__call__` rend {"_erreur": "..."} — un DICT. Le devis l'iterait comme une liste, et un dict itere rend ses CLEFS. `Cluster.rate()` existait pour ca et n'etait appele nulle part ici. Les quatre appels passent desormais par une garde qui nomme la cause, l'hote interroge et le geste a tenter (resolution, VPN, jeton). ET LE DEVIS MESURAIT LE MAUVAIS TENANT. `placement_du_tenant()` lisait `instance/` en dur : viser patient 0 avec SETOPS_INSTANCE mesurait en silence le placement de l'instance montee. Le verdict etait juste — pour l'autre tenant. Les deux portaient les memes quatre valeurs, ce qui est exactement la circonstance ou l'erreur ne se voit pas. C'est la HUITIEME resolution d'instance ou d'inventaire codee en dur trouvee en trois jours ; a ce compte ce n'est plus une serie de bogues, c'est une piece manquante. L'en-tete annoncait aussi « tenant instance » — le nom du lien, pas celui du tenant. TROIS TESTS, aucun reseau touche (Cluster simule) : une panne devient un refus lisible ; une reponse qui n'est pas une liste est refusee — c'est le cas silencieux, celui qui franchirait la premiere garde ; le cas nominal traverse sans gene. Branches sur `make test`. Verifie ensuite contre le cluster reel : le devis nomme « OPS-Patient0 » et confirme ses quatre objets (asgard, TrueNAS, vmbr1, gabarit 99998). make test OK ; make verifier 38/38 ; make ci 38/38. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 21:26:32 -04:00
python3 scripts/tests/test_devis_placement.py
sonder : rapporter ce qui distingue, au lieu d un echec Trois faux diagnostics en une journee, tous dus a l instrument et aucun au composant. curl et bash /dev/tcp ecrasent quatre causes incompatibles dans le meme mot : ouvert, une POLITIQUE qui refuse, une machine ABSENTE, et la frontiere muette. LE MEME CODE DIT DEUX CHOSES SELON LE DELAI, et c est la distinction qui a coute le plus cher : EHOSTUNREACH immediat = pas de route ; le meme apres trois secondes = il n y a pas de machine, c est l ARP qui renonce. Les confondre a fait appliquer un pare-feu pour reparer un vide. est pure, donc gardee par un test qui exige que les deux ne se lisent jamais pareil. LA GARDE ECHOUAIT DU COTE SILENCIEUX. L outil dit par quelle SOURCE le paquet part et refuse de conclure sur une passerelle anycast — le piege qui m a fait declarer muet un REJECT qui emettait bien ses RST. Ma premiere version rendait « pas anycast » quand inventory_rules manquait, c est-a-dire sur un hyperviseur ou l on a copie le seul fichier : precisement la ou le piege se produit. Une garde qui echoue doit crier, pas se taire. ET « NON CONCLUANT » EST UN RESULTAT : sur une source anycast et un silence, l outil ne tranche pas, il dit quoi faire — compter les paquets du cote qui refuse, ou sonder depuis une VM. TCP seulement, et c est dit : UDP n a pas de poignee. Valide contre le reel depuis ops-01 : trois cas sur quatre, le quatrieme attendant deux machines vivantes dans un meme tenant. make verifier : vert. make prouver : CONFORME, 53 OK, 0 echec, 0 saute. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-08-28 17:02:20 -04:00
python3 scripts/tests/test_sonder.py
# LA SONDE QUI RAPPORTE CE QUI DISTINGUE (2026-08-28). Trois faux diagnostics en une
# journee, tous dus a l'instrument : `curl` et `bash /dev/tcp` ecrasent « la machine
# n'existe pas », « une politique refuse » et « la frontiere se tait » dans le meme mot.
inseminer : le geste sort de mes mains et entre dans le depot L insemination avait ete conduite A LA MAIN depuis le runner du site — hors du depot, donc sans preuve. Elle a maintenant sa cible : make inseminer TENANT=OPS-Chezlepro TENANT= PLUTOT QUE LE SYMLINK instance : le runner du SITE amorce PLUSIEURS locataires ; pointer un lien global sur l un d eux le ferait se prendre pour ce tenant. Il en NOMME un par commande. Ca borne aussi le couplage que creer-vm imposait en silence — rien ne disait sur quels tenants ce lien pouvait pointer. L HOTE SE DERIVE : celui qui porte serveur_ops_tenant. Meme critere que le flux d insemination et que la cle SSH du runner — le meme mot borne les trois pouvoirs. P54 GARDE LA LIGNE DE PARTAGE la ou elle glisserait sans bruit. Les deux couches retenues sont les seules qui ne reclament aucun secret. Le jour ou l on en ajouterait une, le deploiement echouerait chez le tenant sur une valeur vide, et ce message ne dirait pas qu un POUVOIR a ete franchi. Controle negatif : ajouter client_pki, la couche suivante, fait echouer la preuve. UN GARDE-FOU EXISTANT A INTERCEPTE UNE INSEMINATION MAL DIRIGEE. Le make parent exporte SETOPS_INVENTAIRE ; ma resolution en heritait et visait l inventaire d un AUTRE ecosysteme. Le refus vient d inventory_rules, pas de la cible — exactement l erreur qu un runner servant plusieurs locataires commettrait en silence. make verifier : vert. make prouver : CONFORME, 54 OK, 0 echec, 0 saute. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-08-28 17:14:37 -04:00
# L'INSEMINATION, CODIFIEE (2026-08-28). Elle a d'abord ete conduite A LA MAIN depuis le
# runner du site — donc hors du depot, donc sans preuve, ce que ce projet refuse.
#
# `TENANT=` PLUTOT QUE LE SYMLINK `instance`. Le runner du SITE materialise et amorce
# PLUSIEURS tenants ; pointer un lien global sur l'un d'eux le ferait se prendre pour ce
# tenant. Il en NOMME un par commande, et `SETOPS_INSTANCE` suffit a le dire — verifie le
# 2026-08-28. C'est aussi ce qui borne le couplage que `creer-vm` imposait en silence :
# rien ne disait sur quels tenants ce lien avait le droit de pointer, ni jusqu'a quand.
# `env -u SETOPS_INVENTAIRE` EST OBLIGATOIRE, ET UN GARDE-FOU L'A PROUVE.
#
# Le make parent EXPORTE SETOPS_INVENTAIRE, derive de l'instance montee. Sans le retirer,
# la resolution du tenant demande heritait de l'inventaire d'un AUTRE ecosysteme :
#
# REFUS : SETOPS_INVENTAIRE designe un inventaire HORS de l'instance demandee
#
# Le refus vient d'inventory_rules, pas d'ici — il a intercepte une insemination qui
# aurait vise le mauvais ecosysteme (2026-08-28). C'est exactement le genre d'erreur
# qu'un runner de site, qui sert plusieurs locataires, commettrait en silence.
.PHONY: inseminer
inseminer: ansible-runtime ## Le SITE amorce le runner d'un tenant — TENANT=<dossier> [HOTE=<nom>]
@set -e; \
if [[ -z "$(TENANT)" ]]; then \
printf '%s\n' 'Refus: relancer avec TENANT=<dossier frere>, ex. TENANT=OPS-Chezlepro.'; \
exit 2; \
fi; \
if [[ ! -d "../$(TENANT)/plan" ]]; then \
printf '%s\n' 'Refus: ../$(TENANT)/plan introuvable — ce n est pas un ecosysteme.'; \
exit 2; \
fi; \
inv="$$(env -u SETOPS_INVENTAIRE SETOPS_INSTANCE=../$(TENANT) python3 -c 'import sys; sys.path.insert(0,"scripts"); from inventory_rules import inventaire_de; print(inventaire_de() or "")')"; \
if [[ -z "$$inv" || ! -f "$$inv" ]]; then \
printf '%s\n' "Refus: aucun inventaire pour $(TENANT). Le generer chez lui (make instancier)."; \
exit 2; \
fi; \
hote="$(HOTE)"; \
if [[ -z "$$hote" ]]; then \
hote="$$(python3 -c 'import sys,yaml; d=yaml.safe_load(open(sys.argv[1])) or {}; p=[d.get("all",{})]; out=[]; \
[ (out.extend((n.get("children") or {}).get("serveur_ops_tenant",{}).get("hosts",{}) or {}), p.extend((n.get("children") or {}).values())) for n in iter(lambda: p.pop() if p else None, None) ]; print(" ".join(sorted(set(out))))' "$$inv")"; \
fi; \
if [[ -z "$$hote" ]]; then \
printf '%s\n' "Refus: $(TENANT) ne declare aucun hote portant serveur_ops_tenant."; \
printf '%s\n' "L insemination vise LE RUNNER d un tenant, jamais une machine ordinaire."; \
exit 2; \
fi; \
if (( $$(wc -w <<< "$$hote") > 1 )); then \
printf '%s\n' "Refus: plusieurs runners declares ($$hote). Nommer lequel avec HOTE=."; \
exit 2; \
fi; \
printf 'Insemination de %s chez %s — couches sans secret : %s\n\n' "$$hote" "$(TENANT)" "$(COUCHES_INSEMINATION)"; \
for groupe in $(COUCHES_INSEMINATION); do \
printf '=== %s ===\n' "$$groupe"; \
SETOPS_INSTANCE="../$(TENANT)" SETOPS_INVENTAIRE="$$inv" \
ansible-playbook -i "$$inv" "$(DOSSIER_PLAYBOOKS_GROUPES)/$$groupe.yml" --limit "$$hote"; \
done; \
printf '\n%s\n' "$$hote porte desormais le moteur, son plan et la carte de la fabric."; \
printf '%s\n' "IL N EST PAS ARME : ni voute, ni cle — le site ne les detient pas."; \
printf '%s\n' "L armer est le geste d un humain, depuis son poste :"; \
printf '%s\n' " make appliquer GROUPE=serveur_ops_tenant (voir docs/filiation-emancipation.md)"
emancipation : l instrument de la quatrieme ligne — couper, pas sonder docs/filiation-emancipation.md decrit quatre temps et n en outillait que trois. Le quatrieme est celui qu on oublie : « une emancipation non prouvee est une emancipation non faite ». SONDER NE PROUVE RIEN. Verifier que le service local repond ne dit pas si l amont sert encore — le depot le disait deja du cache : « tant qu internet repond, un apt update qui reussit ne dit pas d ou vient l octet ». L instrument COUPE donc l amont et refait marcher la chose. LE MEME ESSAI REND LES DEUX VERDICTS, et c est ce qui le rend honnete : coupe, la fonction marche -> EMANCIPE, et c est prouve coupe, la fonction casse -> PAS EMANCIPE, dependance prouvee REELLE Le second n est pas un echec de l outil, c est son CONTROLE NEGATIF rendu par la meme commande. Une preuve d emancipation incapable de montrer la dependance qu elle mesure ne prouverait rien le jour ou elle passerait au vert. UN TEMOIN PRECEDE LA COUPURE : la fonction marchait-elle seulement avant ? Sans lui, une panne preexistante se lirait comme une dependance. LA COUPURE EST GARANTIE REVERSIBLE : une TABLE nftables dediee, jamais une regle glissee dans une table existante — elle se retire d un geste et ne peut pas laisser d etat partiel. Le bloc `always` la retire meme si la mesure echoue ou si le play est interrompu, et une tache verifie ensuite qu elle a bien disparu. MESURE LE JOUR DE SA NAISSANCE, les deux verdicts sur du vrai materiel : obs-01 / resolveur PAS EMANCIPE — plus aucune resolution des la coupure forge-01 / artefacts EMANCIPE — apt installe, cache du site coupe Ce second verdict a ete DOUTE puis verifie : apt aurait pu reussir en rejouant des listes fraiches. Refait avec un dossier de listes NEUF, amont coupe : reussit quand meme. Le cache sert vraiment son contenu. make verifier : vert. make prouver : CONFORME, 55 OK, 0 echec. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-09-01 08:36:28 -04:00
# PROUVER QU'UN LIEN DE FILIATION EST COUPE — la quatrieme ligne de
# `docs/filiation-emancipation.md`, celle qu'on oublie : « une emancipation non prouvee
# est une emancipation non faite ».
#
# GESTE DESTRUCTIF ET REVERSIBLE : il COUPE l'amont pendant quelques secondes pour voir
# si la fonction tient sans lui. Sonder ne prouverait rien — tant que l'amont repond, une
# fonction qui marche ne dit pas d'ou vient l'octet.
#
# LE MEME ESSAI REND LES DEUX VERDICTS, et c'est voulu : « emancipe, prouve » quand la
# fonction tient, « pas emancipe, dependance REELLE » quand elle casse. Le second est le
# controle negatif du premier.
.PHONY: emancipation-prouver
emancipation-prouver: ansible-runtime _instance-requise ## Prouve qu un lien est coupe — SERVICE=<artefacts|genome|resolveur> [HOTE=] CONFIRMER=true
@set -e; \
if [[ -z "$(SERVICE)" ]]; then \
printf '%s\n' 'Refus: relancer avec SERVICE=<artefacts|genome|resolveur>.'; \
exit 2; \
fi; \
if [[ "$(CONFIRMER)" != "true" ]]; then \
printf '%s\n' 'Refus: cette preuve COUPE l amont quelques secondes pour mesurer.'; \
printf '%s\n' 'La coupure est retiree quoi qu il arrive (bloc `always`), mais'; \
printf '%s\n' 'pendant ce temps la fonction eprouvee peut echouer sur cet hote.'; \
printf '%s\n' 'Relancer avec CONFIRMER=true.'; \
exit 2; \
fi; \
ansible-playbook -i $(INVENTAIRE_PRODUCTION) playbooks/maintenance/emancipation-prouver.yml \
-e emancipation_service=$(SERVICE) \
$(if $(HOTE),-e emancipation_hotes=$(HOTE)) $(ARGS)
sonder : rapporter ce qui distingue, au lieu d un echec Trois faux diagnostics en une journee, tous dus a l instrument et aucun au composant. curl et bash /dev/tcp ecrasent quatre causes incompatibles dans le meme mot : ouvert, une POLITIQUE qui refuse, une machine ABSENTE, et la frontiere muette. LE MEME CODE DIT DEUX CHOSES SELON LE DELAI, et c est la distinction qui a coute le plus cher : EHOSTUNREACH immediat = pas de route ; le meme apres trois secondes = il n y a pas de machine, c est l ARP qui renonce. Les confondre a fait appliquer un pare-feu pour reparer un vide. est pure, donc gardee par un test qui exige que les deux ne se lisent jamais pareil. LA GARDE ECHOUAIT DU COTE SILENCIEUX. L outil dit par quelle SOURCE le paquet part et refuse de conclure sur une passerelle anycast — le piege qui m a fait declarer muet un REJECT qui emettait bien ses RST. Ma premiere version rendait « pas anycast » quand inventory_rules manquait, c est-a-dire sur un hyperviseur ou l on a copie le seul fichier : precisement la ou le piege se produit. Une garde qui echoue doit crier, pas se taire. ET « NON CONCLUANT » EST UN RESULTAT : sur une source anycast et un silence, l outil ne tranche pas, il dit quoi faire — compter les paquets du cote qui refuse, ou sonder depuis une VM. TCP seulement, et c est dit : UDP n a pas de poignee. Valide contre le reel depuis ops-01 : trois cas sur quatre, le quatrieme attendant deux machines vivantes dans un meme tenant. make verifier : vert. make prouver : CONFORME, 53 OK, 0 echec, 0 saute. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-08-28 17:02:20 -04:00
.PHONY: sonder
sonder: ## Sonde une cible et DIT ce qui distingue — CIBLE=<hote|ip> [PORTS="22 443"]
@if [[ -z "$(CIBLE)" ]]; then \
printf '%s\n' 'Refus: relancer avec CIBLE=<hote du plan, ou adresse>.'; \
printf '%s\n' 'Ex. make sonder CIBLE=infra-dns-01 PORTS="53 22"'; \
exit 2; \
fi
@python3 scripts/sonder.py $(CIBLE) $(or $(PORTS),22) $(if $(DELAI),--delai $(DELAI))
.PHONY: verifier
verifier: lint test inventaire-verifier site-verifier flux-verifier syntaxe ## Rejoue les preuves SANS reecrire le rapport (verification rapide)
python3 scripts/prouver.py --verifier
CI : le harnais ne se declenchait que par memoire Trente-huit preuves, des tests, un lint — et RIEN ne les executait sans qu'un humain tape `make`. Le meilleur atout du depot dependait de ne pas oublier. Il a desormais une CI (.forgejo/workflows/verifier.yml) et une cible qui la rejoue a l'identique : `make ci`. CE QUE LA CI A TROUVE AVANT D'EXISTER. Ecrire le workflow supposait de repondre a une question jamais posee : est-ce qu'un depot PUBLIC, seul, se tient ? Mesure sur un clone nu : non, a cinq endroits. - `make instancier` echouait sur le modele public — le tout premier geste du QUICKSTART. Le Makefile forcait `principal/hosts.yml` alors que le modele vit en `production/` ; sa precedence suit desormais celle du code (fichier, puis REPERTOIRE existant, puis defaut). - P32 parcourait les 54 roles sans regarder ce que l'instance deploie. Elle passait sur l'ecosysteme de reference PARCE QU'IL PORTE TOUT. Or les modeles sont des OFFRES : toute offre plus petite que l'ecosysteme complet echouait son propre harnais, pour des services qu'elle ne vend pas. Le perimetre se lit maintenant du plan (groupes de l'inventaire, puis roles composes par leur playbook). - P24 : le modele public ne declarait aucun reseau d'administration — une flotte qu'on construit et ou l'on n'entre plus. `nftables_admin_ssh` est pose, avec le pourquoi. - P33 : verifier_ports.py codait `instance/inventories/principal/hosts.yml` en dur. - P32 et P24 lisaient le symlink `instance/` au lieu de SETOPS_INSTANCE. Toutes de la MEME FAMILLE que P03 avant-hier : une resolution d'inventaire recopiee, une variable d'environnement qui deborde de sa portee. Le depot en compte SEPT ; deux de plus sont corrigees ici, et la septieme le dit en commentaire plutot que de le taire. `make ci` NE TOUCHE AUCUN SYMLINK : le modele public est monte comme instance jetable, vise par SETOPS_INSTANCE/SETOPS_UNDERLAY, detruit en sortant. Deux details mesures parce que devines faux d'abord : l'instance jetable est un DOSSIER FRERE (la federation se decouvre ainsi ; ailleurs, quatre preuves tombent) ; et SETOPS_UNDERLAY n'est pose QUE pour la verification, sinon l'inventaire est ecrit avec une fabric et regenere avec une autre — la commande fabriquait l'ecart qu'elle denonce. RESULTAT : clone nu sans instance ni frere -> 38 OK, 0 echec, 0 saute. Depot de l'exploitant avec ses 3 instances -> 38 OK, 0 echec, 0 saute. Aucun residu. Et le lint du depot a refuse mon propre fichier de CI avant qu'il ne tourne une seule fois (`on:` lu par YAML comme le booleen vrai). Le harnais mordait deja. A AJUSTER AU PREMIER PASSAGE, ecrit en tete du workflow : l'etiquette `runs-on` doit correspondre a un runner Forgejo enregistre, et le runner a besoin du reseau pour pip et ansible-galaxy. Le vert de cette CI dira que le moteur et son modele public se tiennent — pas que la flotte va bien : aucune VM jointe, aucune voute. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 19:29:46 -04:00
# Ce que la CI execute, et que tu peux rejouer a l'identique.
#
# Le modele PUBLIC est monte comme instance jetable et vise par SETOPS_INSTANCE /
# SETOPS_UNDERLAY : AUCUN symlink n'est touche, ton instance reste montee pendant
# l'execution. Deux details ne sont pas negociables, mesures le 2026-08-20 :
#
# - l'instance jetable est un DOSSIER FRERE du depot, pas un /tmp. La federation se
# decouvre par les dossiers freres (`devis_reseau.decouvrir`) : ailleurs, elle est
# invisible, et quatre preuves tombent en disant « aucun tenant federe decouvert » ;
# - `env -u SETOPS_INVENTAIRE`, sinon la variable exportee par CE make survit dans le
# sous-make et designe l'inventaire de l'instance ACTIVE — le piege de P03.
#
# Et SETOPS_UNDERLAY n'est pose QUE pour la verification, jamais pour l'application :
# l'inventaire de l'instance jetable doit etre ecrit avec la meme fabric que celle avec
# laquelle P03 le REGENERERA ensuite. En posant l'underlay du modele des l'application,
# `make ci` passait sur un depot nu et echouait chez un exploitant dont le symlink
# `underlay.yml` designe une autre fabric — quatre hotes « en ecart », pour un ecart
# fabrique par la commande elle-meme.
# LE GENOME : les depots sans lesquels cet ecosysteme ne peut pas etre refabrique, et la
# PARENTE qu'il inscrit (de quel moteur il descend, a quel commit, sous quelle etiquette
# signee). Perdre les VM coute du temps ; perdre ces depots coute l'ecosysteme.
.PHONY: genome genome-inscrire genome-verifier
genome: ## Les depots requis a la reproduction de cet ecosysteme, et leur etat
python3 scripts/genome.py lister
genome-inscrire: ## Inscrit la parente dans l'instance (instance/parente.yml)
python3 scripts/genome.py inscrire
genome-verifier: ## La parente inscrite tient-elle encore ? (code de sortie)
python3 scripts/genome.py verifier
CI : le harnais ne se declenchait que par memoire Trente-huit preuves, des tests, un lint — et RIEN ne les executait sans qu'un humain tape `make`. Le meilleur atout du depot dependait de ne pas oublier. Il a desormais une CI (.forgejo/workflows/verifier.yml) et une cible qui la rejoue a l'identique : `make ci`. CE QUE LA CI A TROUVE AVANT D'EXISTER. Ecrire le workflow supposait de repondre a une question jamais posee : est-ce qu'un depot PUBLIC, seul, se tient ? Mesure sur un clone nu : non, a cinq endroits. - `make instancier` echouait sur le modele public — le tout premier geste du QUICKSTART. Le Makefile forcait `principal/hosts.yml` alors que le modele vit en `production/` ; sa precedence suit desormais celle du code (fichier, puis REPERTOIRE existant, puis defaut). - P32 parcourait les 54 roles sans regarder ce que l'instance deploie. Elle passait sur l'ecosysteme de reference PARCE QU'IL PORTE TOUT. Or les modeles sont des OFFRES : toute offre plus petite que l'ecosysteme complet echouait son propre harnais, pour des services qu'elle ne vend pas. Le perimetre se lit maintenant du plan (groupes de l'inventaire, puis roles composes par leur playbook). - P24 : le modele public ne declarait aucun reseau d'administration — une flotte qu'on construit et ou l'on n'entre plus. `nftables_admin_ssh` est pose, avec le pourquoi. - P33 : verifier_ports.py codait `instance/inventories/principal/hosts.yml` en dur. - P32 et P24 lisaient le symlink `instance/` au lieu de SETOPS_INSTANCE. Toutes de la MEME FAMILLE que P03 avant-hier : une resolution d'inventaire recopiee, une variable d'environnement qui deborde de sa portee. Le depot en compte SEPT ; deux de plus sont corrigees ici, et la septieme le dit en commentaire plutot que de le taire. `make ci` NE TOUCHE AUCUN SYMLINK : le modele public est monte comme instance jetable, vise par SETOPS_INSTANCE/SETOPS_UNDERLAY, detruit en sortant. Deux details mesures parce que devines faux d'abord : l'instance jetable est un DOSSIER FRERE (la federation se decouvre ainsi ; ailleurs, quatre preuves tombent) ; et SETOPS_UNDERLAY n'est pose QUE pour la verification, sinon l'inventaire est ecrit avec une fabric et regenere avec une autre — la commande fabriquait l'ecart qu'elle denonce. RESULTAT : clone nu sans instance ni frere -> 38 OK, 0 echec, 0 saute. Depot de l'exploitant avec ses 3 instances -> 38 OK, 0 echec, 0 saute. Aucun residu. Et le lint du depot a refuse mon propre fichier de CI avant qu'il ne tourne une seule fois (`on:` lu par YAML comme le booleen vrai). Le harnais mordait deja. A AJUSTER AU PREMIER PASSAGE, ecrit en tete du workflow : l'etiquette `runs-on` doit correspondre a un runner Forgejo enregistre, et le runner a besoin du reseau pour pip et ansible-galaxy. Le vert de cette CI dira que le moteur et son modele public se tiennent — pas que la flotte va bien : aucune VM jointe, aucune voute. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 19:29:46 -04:00
.PHONY: ci
ci: ## Verifie le depot comme la CI : modele public monte a l'ecart, sans toucher ton instance
@set -e ; \
frere="../instance-ci-$$$$" ; \
trap 'rm -rf "$$frere"' EXIT ; \
rm -rf "$$frere" ; cp -r exemples/modeles/socle "$$frere" ; \
env -u SETOPS_INVENTAIRE SETOPS_INSTANCE="$$frere" \
$(MAKE) --no-print-directory instancier-appliquer FORCE=1 ; \
env -u SETOPS_INVENTAIRE SETOPS_INSTANCE="$$frere" \
SETOPS_UNDERLAY="$$frere/underlay.yml" \
$(MAKE) --no-print-directory verifier
# Harnais de preuve : rejoue les preuves automatisables du registre et ecrit
# docs/audit/preuve-<date>.md (piece justificative horodatee, rejouable).
# `make verifier` l'appelle en mode --verifier (preuves seules, aucun rapport ecrit).
.PHONY: prouver
prouver: ansible-runtime _instance-requise ## Execute les preuves et ecrit docs/audit/preuve-<date>.md
python3 scripts/prouver.py
.PHONY: inventaire hote-planifier hote-ajouter hote-groupes hote-afficher appliquer deployer deployer-groupe cloner-vm creer-vm config inventaire-ui inventaire-verifier inventaire-lister inventaire-graphe inventaire-hote inventaire-lab inventaire-production instance-utiliser instance-courante
inventaire: inventaire-production ## Verifie l'inventaire et en affiche le graphe (lab puis production)
# Bascule le symlink 'instance' vers un autre dépôt d'instance (séparation par
# instance : prod vs bac à sable). Ex. : make instance-utiliser NOM=OPS-Chezlepro-lab
instance-utiliser: ## Bascule l'instance active (symlink instance/) vers un dossier frere — NOM=<dossier>
@if [[ -z "$(NOM)" ]]; then printf '%s\n' "Usage: make instance-utiliser NOM=<dossier-frère> (ex. OPS-Chezlepro-lab)"; exit 2; fi
@if [[ ! -d "../$(NOM)" ]]; then printf '%s\n' "Introuvable: ../$(NOM)"; exit 2; fi
@if [[ -e instance && ! -L instance ]]; then printf '%s\n' "Refus: 'instance' existe et n'est pas un symlink."; exit 2; fi
@rm -f instance && ln -s "../$(NOM)" instance
@printf 'instance -> %s\n' "$$(readlink instance)"
instance-courante: ## Affiche vers quel ecosysteme pointe l'instance active
@printf 'instance -> %s\n' "$$(readlink instance 2>/dev/null || echo '(non monté)')"
# Vue d'ensemble : toutes les instances de la fédération, l'active (*), leur index,
# plage VLAN, statut fédéré/prod ; signale les collisions d'index. Lecture seule.
instances: ## Liste les ecosystemes decouverts (dossiers freres) et signale les collisions d'index
@python3 scripts/instances.py
# (Re)génère le plan de recette (docs/audit/plan-de-recette.md) depuis les exercices
# du wiki. La preuve P22 vérifie qu'il reste à jour.
plan-recette: ## Regenere docs/audit/plan-de-recette.md depuis le wiki
@python3 scripts/plan_recette.py
# Liste les modèles disponibles (socle + SETOPS_MODELES) pour créer une instance.
instance-modeles: ## Liste les modeles d'ecosysteme disponibles
@python3 scripts/instance_creer.py --lister-modeles
# Crée un dépôt d'instance frère depuis un modèle. Ne bascule pas le symlink.
# Ex. : make instance-creer NOM=OPS-ClientX MODELE=socle INDEX=4
instance-creer: ## Cree un nouvel ecosysteme depuis un modele — NOM=<nom> MODELE=<modele>
@python3 scripts/instance_creer.py --nom "$(NOM)" --modele "$(MODELE)" \
$(if $(INDEX),--index $(INDEX),)
# Crée un MODÈLE (dépôt privé). Deux modes :
# base : copier un modèle générique -> make model-creer MODE=base BASE=identite NOM=maison-obnl
# instance : promouvoir une instance -> make model-creer MODE=instance SOURCE=OPS-Chezlepro NOM=cabinet
model-creer: ## Cree un modele d'ecosysteme — MODE=<mode> NOM=<nom>
@python3 scripts/model_creer.py --mode "$(MODE)" --nom "$(NOM)" \
$(if $(BASE),--base $(BASE),) $(if $(SOURCE),--source $(SOURCE),) $(if $(DEST),--dest $(DEST),)
config: ## Affiche la configuration Proxmox lue par le moteur
python3 scripts/config_proxmox.py
inventaire-ui: _instance-requise ## Ouvre la console d'exploitation (GUI web) sur l'inventaire actif
python3 scripts/inventory_gui.py --inventaire $(FICHIER_INVENTAIRE)
hote-ajouter hote-planifier hote-groupes:
@printf '%s\n' 'Cible depreciee: l inventaire est GENERE depuis le plan, il ne s edite plus a la main.'
@printf '%s\n' 'Declare ou modifie l hote dans instance/plan/serveurs.yml (ou la vue Serveurs du GUI), puis :'
@printf '%s\n' ' make instancier-appliquer'
@printf '%s\n' '(creer-vm lit desormais VMID/IP/VLAN/passerelle directement dans l inventaire genere.)'
@exit 2
hote-afficher: ansible-runtime ## Affiche tout ce que le plan derive pour un hote — HOTE=<nom>
@if [[ -z "$(HOTE)" ]]; then \
printf '%s\n' 'Refus: relancer avec HOTE=nom_hote.'; \
exit 2; \
fi
python3 scripts/inventory_host.py --inventaire $(FICHIER_INVENTAIRE) afficher --hote $(HOTE)
appliquer: ansible-runtime ## Applique un groupe a la flotte — GROUPE=<groupe>
@if [[ -z "$(GROUPE)" ]]; then \
printf '%s\n' 'Refus: relancer avec GROUPE=nom_groupe.'; \
exit 2; \
fi
@if [[ ! -f "$(DOSSIER_PLAYBOOKS_GROUPES)/$(GROUPE).yml" ]]; then \
printf '%s\n' 'Refus: aucun playbook pour ce groupe: $(DOSSIER_PLAYBOOKS_GROUPES)/$(GROUPE).yml'; \
exit 2; \
fi
python3 scripts/inventory_host.py --inventaire $(INVENTAIRE_PRODUCTION) --dependances $(FICHIER_DEPENDANCES) verifier-dependances-groupe --groupe $(GROUPE)
ansible-playbook -i $(INVENTAIRE_PRODUCTION) "$(DOSSIER_PLAYBOOKS_GROUPES)/$(GROUPE).yml" --limit '$(GROUPE):&$(GROUPE_HOTES_ACTIFS)'
creer-vm : prouver la materialisation sans entrer chez le tenant `creer-vm` confirmait son succes en attendant une reponse SSH. Le runner du SITE materialise le terrain de TOUS les tenants, mais la frontiere lui refuse d'entrer chez eux — c'est le sens meme de leur isolation. La premiere VM qu'il a creee a donc ete declaree en echec apres 600 secondes alors qu'elle tournait, avec l'adresse exacte que le plan lui destinait : Attente de SSH sur ops-01 ............ ECHEC: injoignable apres 600s. LA TENTATION ETAIT D'OUVRIR LE SSH du runner vers tous les tenants. Ca aurait repare la mesure en detruisant ce qu'elle protege : une machine capable d'entrer chez chaque locataire est precisement ce que cette architecture refuse d'avoir. L'agent invite repond sans rien ouvrir — l'API des hyperviseurs est deja le flux par lequel la VM vient d'etre creee, donc qui peut la creer peut la voir naitre — et il PROUVE DAVANTAGE. « Quelque chose ecoute sur le port 22 » ne dit ni quel systeme a demarre, ni si cloud-init a pose la bonne adresse. Sur ops-01 : 10.17.19.41 portee sur asgard — Debian GNU/Linux 13 (trixie) 6.12.101 Ses echecs distinguent deux causes tres differentes : « la machine demarre mais rapporte une AUTRE adresse — cloud-init, ou le pont sur lequel elle est posee », et « introuvable sur la fabric ». ATTENDRE LA DISPONIBILITE A CHANGE DE MAIN. Guetter cloud-init et la liberation de dpkg appartient a qui va CONFIGURER : `deployer` le fait desormais, la ou il se contentait d'un `ping` unique. Il y gagne ce qu'il n'avait pas — attendre le verrou APT, faute de quoi la premiere couche echouait dessus. Contrepartie assumee : un nom d'hote errone patiente au lieu d'echouer vite ; echouer vite interdirait de chainer creation et deploiement, le geste central d'une reconstruction. P52 garde le couplage ferme. Deux controles negatifs verifies. make prouver : CONFORME, 52 OK, 0 echec. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 22:48:49 -04:00
# ATTENDRE PLUTOT QUE CONSTATER (2026-08-27). `deployer` faisait un `ping` unique
# (`_verifier-acces-hote`). Depuis que `creer-vm` ne guette plus le SSH — il prouve la
# materialisation par l'agent invite, seul geste que le runner du SITE ait le droit de
# faire chez un tenant — c'est ICI qu'il faut attendre : enchainer `creer-vm` puis
# `deployer` tomberait sinon sur une machine encore en cloud-init. On y gagne au passage
# ce que le `ping` unique ne faisait pas : attendre que `dpkg` se libere, faute de quoi
# la premiere couche echoue sur un verrou APT.
#
# CONTREPARTIE ASSUMEE : un nom d'hote errone ne fait plus echouer tout de suite, il
# patiente ATTENTE_HOTE secondes. Echouer vite sur l'injoignable interdirait de chainer
# la creation au deploiement — le geste central d'une reconstruction.
deployer: _instance-requise ## Deploie un hote, couche par couche, dans l'ordre du graphe — HOTE=<nom>
@set -e; \
if [[ -z "$(HOTE)" ]]; then \
printf '%s\n' 'Refus: relancer avec HOTE=nom_hote.'; \
exit 2; \
fi; \
python3 scripts/inventory_host.py --inventaire $(FICHIER_INVENTAIRE) verifier-actif --hote $(HOTE); \
python3 scripts/inventory_host.py --inventaire $(FICHIER_INVENTAIRE) --dependances $(FICHIER_DEPENDANCES) verifier-dependances-hote --hote $(HOTE); \
playbooks="$$(python3 scripts/inventory_host.py --inventaire $(FICHIER_INVENTAIRE) playbooks --hote $(HOTE) --dossier-playbooks $(DOSSIER_PLAYBOOKS_GROUPES))"; \
if [[ -z "$$playbooks" ]]; then \
printf '%s\n' 'Refus: aucun playbook applicable pour HOTE=$(HOTE).'; \
exit 2; \
fi; \
$(VAULT_UNE_FOIS) \
creer-vm : prouver la materialisation sans entrer chez le tenant `creer-vm` confirmait son succes en attendant une reponse SSH. Le runner du SITE materialise le terrain de TOUS les tenants, mais la frontiere lui refuse d'entrer chez eux — c'est le sens meme de leur isolation. La premiere VM qu'il a creee a donc ete declaree en echec apres 600 secondes alors qu'elle tournait, avec l'adresse exacte que le plan lui destinait : Attente de SSH sur ops-01 ............ ECHEC: injoignable apres 600s. LA TENTATION ETAIT D'OUVRIR LE SSH du runner vers tous les tenants. Ca aurait repare la mesure en detruisant ce qu'elle protege : une machine capable d'entrer chez chaque locataire est precisement ce que cette architecture refuse d'avoir. L'agent invite repond sans rien ouvrir — l'API des hyperviseurs est deja le flux par lequel la VM vient d'etre creee, donc qui peut la creer peut la voir naitre — et il PROUVE DAVANTAGE. « Quelque chose ecoute sur le port 22 » ne dit ni quel systeme a demarre, ni si cloud-init a pose la bonne adresse. Sur ops-01 : 10.17.19.41 portee sur asgard — Debian GNU/Linux 13 (trixie) 6.12.101 Ses echecs distinguent deux causes tres differentes : « la machine demarre mais rapporte une AUTRE adresse — cloud-init, ou le pont sur lequel elle est posee », et « introuvable sur la fabric ». ATTENDRE LA DISPONIBILITE A CHANGE DE MAIN. Guetter cloud-init et la liberation de dpkg appartient a qui va CONFIGURER : `deployer` le fait desormais, la ou il se contentait d'un `ping` unique. Il y gagne ce qu'il n'avait pas — attendre le verrou APT, faute de quoi la premiere couche echouait dessus. Contrepartie assumee : un nom d'hote errone patiente au lieu d'echouer vite ; echouer vite interdirait de chainer creation et deploiement, le geste central d'une reconstruction. P52 garde le couplage ferme. Deux controles negatifs verifies. make prouver : CONFORME, 52 OK, 0 echec. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 22:48:49 -04:00
$(MAKE) --no-print-directory _attendre-hote LIMITE="$(HOTE)"; \
$(MAKE) _verifier-privileges-hote LIMITE="$(HOTE)"; \
for playbook in $$playbooks; do \
ansible-playbook -i $(INVENTAIRE_PRODUCTION) "$$playbook" --limit "$(HOTE)"; \
done; \
$(MAKE) verifier-hote LIMITE="$(HOTE)"
.PHONY: site site-verifier deployer-tout
site: ansible-runtime ## Regenere playbooks/site.yml depuis les couches et le graphe de dependances
python3 scripts/orchestrer.py ecrire
ansible-playbook -i $(INVENTAIRE_PRODUCTION) playbooks/site.yml --syntax-check
site-verifier: ## Verifie que playbooks/site.yml correspond aux couches declarees
python3 scripts/orchestrer.py verifier
.PHONY: flux flux-verifier
cles-recenser: ## Montre ce qui n'existe QUE sur ce poste (sans rien ecrire)
python3 scripts/exporter_cles.py --recenser
cles-exporter: ## Sort les cles du poste, chiffrees et relues — VERS=<repertoire>
@# A LANCER SOI-MEME, PAS PAR UN AGENT. `gpg` demande une phrase de passe : elle ne
@# doit passer ni par un journal, ni par le contexte d'un assistant. Taper la commande
@# dans son propre terminal est la seule facon de s'en assurer.
@if [ -z "$(VERS)" ]; then \
printf 'Refus: relancer avec VERS=<repertoire de destination>.\n'; \
printf 'Ex. make cles-exporter VERS=/media/danallaire/CLE\n'; exit 2; fi
python3 scripts/exporter_cles.py --vers "$(VERS)"
.PHONY: cles-recenser cles-exporter
depot-hors-site: ## Copie le depot de sauvegarde du site HORS du site — VERS=<repertoire>
@# Le depot heberge la racine de l'AC, la forge du genome, et l'etat de CHAQUE
@# locataire — sur une seule machine, dans un seul batiment. La copie est opaque
@# (chiffree cote client) : elle se depose chez un pair sans rien lui demander
@# d'autre que de la disponibilite.
@if [ -z "$(VERS)" ]; then \
printf 'Refus: relancer avec VERS=<repertoire de destination>.\n'; \
printf 'Ex. make depot-hors-site VERS=/media/danallaire/setops-coffre\n'; exit 2; fi
@# EN JSON, PAS EN `-e cle=valeur` (2026-09-05). La forme `cle=valeur` DECOUPE SUR
@# LES BLANCS — c'est ainsi qu'on passe plusieurs variables d'un coup. Un chemin
@# contenant une espace y perd donc tout ce qui suit le premier blanc, et l'erreur
@# parle d'un repertoire introuvable au nom tronque : « /media/danallaire/Cle ».
@# Le depot avait deja appris ca pour le clonage de VM ; la lecon ne s'etait pas
@# propagee ici. Quatrieme fois que ce piege est paye.
ansible-playbook -i scripts/site_inventaire.py \
playbooks/maintenance/depot-hors-site.yml -e '{"vers": "$(VERS)"}'
.PHONY: depot-hors-site
cles-compagnons: ## Depose le script de restauration sur une cle DEJA faite — VERS=<repertoire>
@# POUR UNE CLE EXISTANTE. L'archive n'est pas refaite : ni phrase de passe redemandee,
@# ni risque d'ecraser ce qui est deja verifie.
@if [ -z "$(VERS)" ]; then \
printf 'Refus: relancer avec VERS=<repertoire de la cle>.\n'; exit 2; fi
python3 scripts/exporter_cles.py --vers "$(VERS)" --compagnons
cles-restaurer: ## Remet les cles en place depuis une archive — ARCHIVE=<fichier>
@# EN VRAI, ON N'UTILISE PAS CETTE CIBLE. Le jour ou l'on restaure, le depot n'est pas
@# la — le cloner demande la cle SSH qu'on essaie justement de recuperer. On lance
@# alors `restaurer_cles.py` DEPUIS LA CLE USB, ou les trois commandes du LISEZ-MOI.
@# Cette cible existe pour eprouver la manoeuvre pendant qu'on a encore le choix.
@if [ -z "$(ARCHIVE)" ]; then \
printf 'Refus: relancer avec ARCHIVE=<chemin de l archive .tar.gpg>.\n'; exit 2; fi
python3 scripts/restaurer_cles.py "$(ARCHIVE)" $(if $(FORCER),--forcer)
.PHONY: cles-compagnons cles-restaurer
paquets tiers : passer par le cache du controleur, plus par Internet Trois depots tiers etaient en HTTPS, et client_artefacts pose Acquire::https::Proxy DIRECT — sans quoi le cache du site refuse les tunnels et aucun depot tiers n est joignable. La ligne est juste ; sa consequence ne l avait pas ete vue : ces trois depots CONTOURNENT le cache, et chaque VM neuve allait les chercher sur Internet a sa naissance. step-cli et alloy sont poses par des integrations UNIVERSELLES. Sans lien, une machine neuve n obtenait ni son client d autorite ni ses metriques — elle n entrait dans aucun flux chiffre. Dernier obstacle a une reconstruction hors ligne, et il tenait dans un mot. make cacher-paquets tire les 21 deb aux versions epinglees, empreinte SHA256 verifiee depuis l index du depot. Le role partage paquets_tiers les depose et les installe EN UN SEUL appel a apt — il sait resoudre un ensemble de fichiers locaux qui se dependent, la ou paquet par paquet echouerait sur l ordre (Icinga en apporte dix-sept). Six roles branches ; chacun retombe sur le depot distant pour ce que le cache n a PAS fourni, et rien d autre. Eprouve pour de vrai : packages.smallstep.com renvoye vers 127.0.0.1, step-cli desinstalle, cache local efface. Le role rejoue installe 0.30.6-1 depuis le cache et la machine obtient ses certificats. Zero echec. Deux defauts de mon propre outil, trouves en le construisant : Smallstep sert son index NON COMPRESSE (404 sur .gz) donc step-cli n etait jamais mis en cache ; et un depot injoignable faisait continue AVANT d incrementer le total, si bien que le script rapportait 20 sur 20 alors qu il en manquait un. Une garde qui ne peut pas echouer ne garde rien — troisieme fois cette semaine. Corrige puis eprouve en le faisant echouer. Reste hors ligne : NTP externe, et l expedition des alertes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-09-05 08:22:14 -04:00
cacher-paquets: ## Tire les paquets NON DEBIAN dans le cache du controleur (a faire en ligne)
@# CE QUI REND LA RECONSTRUCTION HORS LIGNE POSSIBLE. Trois depots tiers sont en
@# HTTPS et contournent le cache du site (`Acquire::https::Proxy "DIRECT"`). Sans
@# cette cible, chaque VM neuve va chercher `step-cli` et `alloy` sur Internet — deux
@# integrations UNIVERSELLES. A lancer depuis un poste connecte ; le deploiement, lui,
@# n'en aura plus besoin.
python3 scripts/cacher_paquets.py
.PHONY: flux
flux: ansible-runtime ## Regenere le registre des flux et les regles nftables depuis les meta/flux.yml
python3 scripts/resoudre_flux.py registre
python3 scripts/resoudre_flux.py nftables
durcissement : le site recoit enfin serveur_durci Les six machines du SITE — racine de l AC, forge, cache, resolveur, depot de sauvegarde, runner — ne recevaient que serveur_debian. Ni auditd, ni fail2ban, ni apparmor, ni sysctl, ni pare-feu. Le commentaire au-dessus de GROUPE_SOCLE affirmait pourtant le contraire, ce qui rendait l ecart invisible. Le pare-feu du site n avait aucune des trois pieces qui le rendent applicable : - aucune regle generee (resoudre_flux lit un hosts.yml ; le site a un inventaire dynamique) -> commande nftables-site, branchee sur make flux - le chemin du jeu de regles sortait du depot (inventory_dir vaut scripts/ pour un inventaire dynamique) -> host_var explicite - aucun nftables_admin_ssh : l exploitant administre PAR REBOND, la connexion arrive avec l adresse de la patte de frontiere DANS LA ZONE VISEE. Le generateur refuse desormais de produire des regles sans cet intrant. Defaut attrape en LISANT le jeu de regles avant de l appliquer : la regle DNS de site-dns-01 omettait 10.17.0.0/16. _supernets_voisins retire l instance montee — juste chez un tenant, faux du cote du site, ou ca retire le locataire qu on pilote. L appliquer aurait prive de DNS les quinze machines reconstruites la veille. MaxStartups du depot passe en host_var : dans le meta du role, il ne portait que dans son play, et le passage de serveur_durci le remettait au defaut sans rien dire. Un reglage qui depend de l ordre des plays revient en arriere. Verifie depuis un locataire a travers le nouveau pare-feu : cache, forge, depot (2 snapshots) et resolveur repondent ; l AC du site refuse, et c est correct. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-09-02 10:31:21 -04:00
@# Les machines du SITE aussi : leur inventaire est dynamique, donc invisible
@# pour la generation qui lit un `hosts.yml`. Deux absences se cachaient l'une
@# l'autre — aucune regle generee, et aucun pare-feu applique pour s'en rendre
@# compte. Sans underlay monte, la cible ne fait rien plutot que d'echouer.
@python3 scripts/resoudre_flux.py nftables-site || printf 'note : pas de site monte, aucune regle de site generee.\n'
.PHONY: devis-reseau
devis-reseau: ansible-runtime ## Devis switch (VLANs/SVIs/ACLs) du reseau converge, derive des nomenclatures. DIALECTE=cisco|binardat
python3 scripts/devis_reseau.py $(if $(DIALECTE),--dialecte $(DIALECTE),)
frontière nord/sud : devis dérivé, lien de transit et les deux routes La bordure devient un artefact dérivé, comme le devis switch — et le chemin qui y mène est enfin déclaré. `make devis-opnsense` (+ preuve P24) dérive la politique de bordure du registre des flux : les flux `pair: externe`, que `resoudre_flux.py` saute volontairement parce qu'ils relèvent de la frontière et non du pare-feu d'hôte. Aucun port, aucune adresse, aucun nom d'hôte dans le générateur. Le lien manquait dans tous les fichiers : le devis switch ne contenait pas une seule `ip route`. Un réseau underlay portant `passerelle_sortie` le déclare — il vit dans l'underlay et non dans un tenant parce que la frontière route vers TOUS les supernets tenants par le même saut, donc il ne peut dériver d'aucun `index`. `devis-reseau` en tire deux routes : l'aller (sortie générale) et le retour vers l'administration, dont l'absence a coûté la passe de déploiement du 2026-07-29 — la réponse revient au pare-feu par une autre interface que celle où l'état a été créé, et se fait jeter en silence. Les réseaux d'administration viennent de l'intrant `nftables_admin_ssh` : même source unique que la garde anti-lockout des nftables et l'alias SETOPS_ADMIN. Les trois pare-feux et les routes ne peuvent plus diverger. La frontière est réglable depuis la console (section « Frontière » du panneau Intrants) ; les identifiants d'API restent interdits d'écriture par le GUI et vivent dans la voûte. Correctifs de la même passe : - le panneau refusait d'enregistrer les intrants de la frontière : le garde-fou confondait une référence de voûte `{{ vault_* }}` préservée avec un secret soumis. Il regarde désormais la valeur, pas le nom. - `supprimer_vm_debian.yml` ne chargeait que `proxmox.vault.yml` pour ses secrets ; retirer ce reliquat aurait cassé `make detruire`. Aligné sur le playbook de clonage, voûte unique en dernier. - documentation : la voûte est unique, `proxmox.vault.yml` n'est qu'un reliquat de compatibilité. Preuves : 24 OK, 0 échec. Cas de rejet du validateur d'underlay exercés un par un ; résolution du jeton Proxmox vérifiée en exécution réelle. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:32:04 -04:00
.PHONY: devis-opnsense devis-opnsense-verifier
devis-opnsense: ansible-runtime ## Devis OPNsense (frontiere nord/sud), derive du registre des flux (pair: externe)
python3 scripts/devis_opnsense.py $(if $(JSON),--json,)
.PHONY: proxmox-fw-plan proxmox-fw-appliquer
proxmox-fw-plan: ansible-runtime ## Ecart entre le pare-feu est-ouest Proxmox et son devis (aucune ecriture)
python3 scripts/appliquer_proxmox_fw.py
proxmox-fw-appliquer: ansible-runtime ## Reconcilie le pare-feu est-ouest : IPSets, groupes, affectations. CONFIRMER=true
@if [[ "$(CONFIRMER)" != "true" ]]; then \
printf '%s\n' "Refus: cette cible ECRIT sur le pare-feu du cluster (IPSets, groupes, VM)" \
"et RETIRE ce qui est perime. Elle n'active JAMAIS le pare-feu du datacenter." \
"Relire d'abord 'make proxmox-fw-plan', puis: make proxmox-fw-appliquer CONFIRMER=true"; \
exit 2; fi
CONFIRMER=true python3 scripts/appliquer_proxmox_fw.py
2026-08-13 10:11:25 -04:00
.PHONY: placement-plan sdn-plan sdn-appliquer
placement-plan: ansible-runtime ## Le noeud, stockage, pont et gabarit du tenant existent-ils sur ce cluster ? (aucune ecriture)
python3 scripts/devis_placement.py
sdn-plan: ansible-runtime ## Ecart entre le SDN EVPN (+ sortie des VRF) et son devis (aucune ecriture)
python3 scripts/appliquer_sdn.py
sdn-appliquer: ansible-runtime ## Reconcilie le SDN : cree ce qui manque, RETIRE ce qui est perime. CONFIRMER=true
@if [[ "$(CONFIRMER)" != "true" ]]; then \
printf '%s\n' "Refus: cette cible ECRIT sur le cluster (zones, VNets, sous-reseaux) et sur" \
"les noeuds de sortie (/etc/frr/frr.conf.local), et RETIRE ce qui est perime." \
"Relire d'abord 'make sdn-plan', puis: make sdn-appliquer CONFIRMER=true"; \
exit 2; fi
CONFIRMER=true python3 scripts/appliquer_sdn.py
.PHONY: ca-racine
# Ou deposer la racine. Par defaut le repertoire courant : c'est un certificat
# PUBLIC, pas un secret — il n'a rien a faire dans la voute, et tout a faire dans
# le magasin de confiance de qui administre.
CA_RACINE_DEST ?= ./root_ca.crt
ca-racine: ansible-runtime _instance-requise ## Recupere la racine de l'AC interne + son empreinte (a verifier AVANT de l'installer)
@set -e; \
hote="$$(python3 -c "import yaml,sys; d=yaml.safe_load(open('$(FICHIER_INVENTAIRE)')); \
import itertools; \
g=d['all']['children']; \
print(next(iter((g.get('serveur_step_ca') or {}).get('hosts') or {}), ''))" 2>/dev/null || true)"; \
if [[ -z "$$hote" ]]; then \
printf '%s\n' "Refus: aucun hote ne porte 'serveur_step_ca' dans $(FICHIER_INVENTAIRE)." \
"Cette instance n'a pas d'autorite de certification interne."; exit 2; fi; \
ansible -i $(FICHIER_INVENTAIRE) "$$hote" --become \
-m fetch -a "src=/etc/step-ca/certs/root_ca.crt dest=$(CA_RACINE_DEST) flat=yes" >/dev/null; \
printf '%s\n' "Racine de l'AC ecrite dans $(CA_RACINE_DEST) (depuis $$hote)."; \
printf '%s\n' "" " sujet : $$(openssl x509 -in $(CA_RACINE_DEST) -noout -subject | sed 's/^subject=//')"; \
printf '%s\n' " valide : $$(openssl x509 -in $(CA_RACINE_DEST) -noout -enddate | sed 's/^notAfter=//')"; \
printf '%s\n' " empreinte: $$(openssl x509 -in $(CA_RACINE_DEST) -noout -fingerprint -sha256 | sed 's/^.*=//' | tr -d ':' | tr 'A-Z' 'a-z')"; \
printf '%s\n' "" \
"COMPARE l'empreinte avec celle de l'AC avant de l'installer :" \
" make ca-empreinte" \
"" \
"Installer une AC, c'est lui donner le droit de signer N'IMPORTE QUEL nom" \
"pour ton navigateur. La comparaison est ce qui distingue ta racine d'une" \
"racine interceptee — ce n'est pas une formalite." \
"" \
" sudo cp $(CA_RACINE_DEST) /usr/local/share/ca-certificates/setops-root.crt" \
" sudo update-ca-certificates" \
" (Firefox a son propre magasin : Parametres > Certificats > Autorites)"
.PHONY: ca-empreinte
ca-empreinte: ansible-runtime _instance-requise ## Empreinte de la racine, lue SUR l'AC (le temoin de comparaison)
@set -e; \
hote="$$(python3 -c "import yaml; d=yaml.safe_load(open('$(FICHIER_INVENTAIRE)')); \
g=d['all']['children']; \
print(next(iter((g.get('serveur_step_ca') or {}).get('hosts') or {}), ''))" 2>/dev/null || true)"; \
if [[ -z "$$hote" ]]; then \
printf '%s\n' "Refus: aucun hote ne porte 'serveur_step_ca'."; exit 2; fi; \
ansible -i $(FICHIER_INVENTAIRE) "$$hote" --become -m command \
-a "step certificate fingerprint /etc/step-ca/certs/root_ca.crt" 2>/dev/null \
| tail -1 | tr -d ' \r'
devis : make versions-mesurer — l'ecart avec l'amont devient lisible Question de l'exploitant : « ne devrait-on pas prendre les versions les plus recentes ? » Non — resoudre « la derniere » au deploiement detruirait la reproductibilite qu'on vient de prouver en rasant et remontant deux ecosystemes, et ferait de chaque reconstruction une loterie. Six majeures de Forgejo ne s'avalent pas en effet de bord d'un `make`. Mais il avait raison de tirer le fil : L'ECART ETAIT INVISIBLE. Il a fallu quatre requetes a la main pour decouvrir Forgejo 10.0.0 contre v16.0.2 publie, Keycloak 26.0.7 contre 26.7.1, Nextcloud 34.0.1 contre 34.0.2, oauth2-proxy a jour. Le devis ne juge pas, il RENSEIGNE : un retard n'est pas une faute, il sort donc en 0. Monter de version reste un acte explicite. Ce qui le distingue d'une liste ecrite a la main : les versions epinglees sont DERIVEES du depot. La source amont, elle, ne peut pas se deriver — elle depend de l'editeur — et vit dans une table. TOUTE VERSION EPINGLEE SANS ENTREE DANS CETTE TABLE FAIT SORTIR EN ERREUR : sans cette garde, un epinglage ajoute demain vieillirait sans que personne ne le voie. Une exemption reste possible, mais ecrite et motivee (deux le sont : la serie PHP de Debian, la version PostgreSQL vide). Eprouve dans les deux sens : un serveur_redis_version ajoute temporairement fait sortir en 1 en le nommant ; retire, retour a 0. Et il rappelle ce qu'on n'epingle PAS. Grafana n'a aucune version dans le depot — le 13.1.1 vu dans l'historique apt etait ce que son depot servait ce jour-la. Debian, Grafana, smallstep et Icinga prennent ce qu'on leur donne. Deux regimes coexistent dans la meme flotte, et les taire donnerait l'illusion que tout est maitrise. Verifie : versions-mesurer code 0, garde eprouvee, prouver.py 35 OK. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 23:02:16 -04:00
.PHONY: frontiere-plan frontiere-appliquer frontiere-mesurer mtu-mesurer versions-mesurer
devis PostgreSQL et courriel : la serie des devis de service est complete PostgreSQL — rend visibles deux defauts deja vecus ici : un reseau ECRIT dans pg_hba au lieu d'etre derive, et une ligne host en clair la ou il faut hostssl (verrou qui saute sans bruit, les clients verify-full continuant de marcher). Interroge pg_settings, jamais le fichier : un grep de postgresql.conf annonce ssl_cert_file=snakeoil alors que le serveur sert le certificat de l'AC — la valeur vient d'un conf.d que le grep ne voyait pas. L'instrument etait incomplet, pas la configuration. Courriel — chaque maillon interroge la ou il dit la verite : postmap -q pour la resolution LDAP de Postfix, doveadm user pour celle de Dovecot (le maillon exact ou la livraison avait bloque), vraies conversations SMTP/IMAP. Verifie aussi qu'une adresse INEXISTANTE ne resout pas — sinon la boite est un fourre-tout et le devis ne mesure plus rien. Trouve immediatement une divergence reelle : Dovecot connait la boite de sysadmin@chezlepro.internal, Postfix ne sait pas y router (query_filter (mail=%s), et l'attribut mail porte sysadmin@chezlepro.ca). Collision de roles : une identite ne porte qu'une adresse mail et on lui en demande deux — notification joignable hors du systeme, et cle de routage local. Arbitrage a rendre avant correction. Tests negatifs : PostgreSQL 3 ecarts nommes, code 1. Une variable morte trouvee dans une branche que le cas nominal n'emprunte jamais. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 08:53:49 -04:00
courriel-plan: ansible-runtime ## Chaine Postfix -> LDAP -> Dovecot -> IMAP (aucune ecriture)
@rm -f $(SETOPS_INSTANCE)/devis-courriel.json.*
@ansible-playbook -i $(SETOPS_INVENTAIRE) playbooks/maintenance/devis-courriel.yml >/dev/null
@python3 scripts/devis_courriel.py
postgresql-plan: ansible-runtime ## Chiffrement impose et portee des acces PostgreSQL (aucune ecriture)
@rm -f $(SETOPS_INSTANCE)/devis-postgresql.json.*
@ansible-playbook -i $(SETOPS_INVENTAIRE) playbooks/maintenance/devis-postgresql.yml >/dev/null
@python3 scripts/devis_postgresql.py
expositions-plan: ansible-runtime ## Chaque exposition du plan repond-elle ? (edge et poste, aucune ecriture)
@ansible-playbook -i $(SETOPS_INVENTAIRE) playbooks/maintenance/devis-expositions.yml >/dev/null
@python3 scripts/devis_expositions.py
devis : make versions-mesurer — l'ecart avec l'amont devient lisible Question de l'exploitant : « ne devrait-on pas prendre les versions les plus recentes ? » Non — resoudre « la derniere » au deploiement detruirait la reproductibilite qu'on vient de prouver en rasant et remontant deux ecosystemes, et ferait de chaque reconstruction une loterie. Six majeures de Forgejo ne s'avalent pas en effet de bord d'un `make`. Mais il avait raison de tirer le fil : L'ECART ETAIT INVISIBLE. Il a fallu quatre requetes a la main pour decouvrir Forgejo 10.0.0 contre v16.0.2 publie, Keycloak 26.0.7 contre 26.7.1, Nextcloud 34.0.1 contre 34.0.2, oauth2-proxy a jour. Le devis ne juge pas, il RENSEIGNE : un retard n'est pas une faute, il sort donc en 0. Monter de version reste un acte explicite. Ce qui le distingue d'une liste ecrite a la main : les versions epinglees sont DERIVEES du depot. La source amont, elle, ne peut pas se deriver — elle depend de l'editeur — et vit dans une table. TOUTE VERSION EPINGLEE SANS ENTREE DANS CETTE TABLE FAIT SORTIR EN ERREUR : sans cette garde, un epinglage ajoute demain vieillirait sans que personne ne le voie. Une exemption reste possible, mais ecrite et motivee (deux le sont : la serie PHP de Debian, la version PostgreSQL vide). Eprouve dans les deux sens : un serveur_redis_version ajoute temporairement fait sortir en 1 en le nommant ; retire, retour a 0. Et il rappelle ce qu'on n'epingle PAS. Grafana n'a aucune version dans le depot — le 13.1.1 vu dans l'historique apt etait ce que son depot servait ce jour-la. Debian, Grafana, smallstep et Icinga prennent ce qu'on leur donne. Deux regimes coexistent dans la meme flotte, et les taire donnerait l'illusion que tout est maitrise. Verifie : versions-mesurer code 0, garde eprouvee, prouver.py 35 OK. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 23:02:16 -04:00
versions-mesurer: ## De combien nos epinglages ont-ils vieilli ? (interroge les amonts, aucune ecriture)
@python3 scripts/devis_versions.py
mtu : le 1450 de la zone n'atteignait pas les invites Question de l'exploitant : « je ne vois nulle part un MTU a 1450 ». Fondee. Le 1450 existait bien — MTU_OVERLAY_DEFAUT dans underlay.py, dont P23 derive sa garde (transport >= overlay + 50), pose par le devis SDN sur chaque zone, et verifie sur le cluster. Mais il ne se propage pas a la carte de l'invite : les 14 VM tournaient a 1500, et cloner_vm_debian.yml n'avait aucune occurrence de `mtu`. La VM emettait donc des trames que son propre chemin ne pouvait pas encapsuler — la panne que le registre des flux appelle « la plus couteuse a diagnostiquer ». Invisible parce que les 14 VM vivent sur asgard : deux VM du meme hyperviseur communiquent par le pont local, sans encapsulation. Le defaut serait apparu au premier eclatement de la flotte — c'est-a-dire quand il faudra heberger les deux tenants ensemble. Corrige en DEUX endroits, avec `mtu=1` (« herite du pont ») plutot que 1450 en dur : juste en SDN comme hors SDN, et encore juste le jour des trames jumbo. Le GABARIT le porte — proposition de l'exploitant, meilleure que la mienne : elle couvre les clonages qui ne passent pas par le playbook. Et le playbook le repose a chaque clone, parce qu'un gabarit se RECAPTURE et que ce qui n'est pas versionne se perd en silence. make mtu-mesurer (7e devis) rattache chaque hote a sa zone par son pont derive et lit le MTU attendu dans devis_sdn.py. Ecart trouve sur 14 hotes du premier coup, puis confirme corrige. ET J'AI CASSE LA FLOTTE EN CORRIGEANT : applique aux cartes de VM EN MARCHE, le changement detache et rebranche la carte sans que l'invite reconfigure son interface. 0/14 joignables pendant trois minutes. L'agent qemu, independant du reseau invite, a permis de constater et de redemarrer par l'API. Dans le playbook la meme tache s'execute AVANT le demarrage du clone : aucun risque. Sur une VM en service : poser la config, puis redemarrer — une seule d'abord. Verifie : mtu-mesurer CONFORME 14/14 a 1450, prouver.py 35 OK, ansible-lint production. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 17:54:03 -04:00
mtu-mesurer: ansible-runtime ## L'invite porte-t-il le MTU de sa zone SDN ? (aucune ecriture)
@ansible-playbook -i $(SETOPS_INVENTAIRE) playbooks/maintenance/devis-mtu.yml >/dev/null
@python3 scripts/devis_mtu.py
frontiere-mesurer: ansible-runtime ## La frontiere refuse-t-elle ce qui n'est pas declare ? (sonde reelle, aucune ecriture)
@ansible-playbook -i $(SETOPS_INVENTAIRE) playbooks/maintenance/devis-frontiere.yml >/dev/null
@python3 scripts/devis_frontiere.py
devis des certificats : disque contre memoire, et l'AC etait expiree Deuxieme application du patron devis/applicateur aux services. Trouve a la premiere execution : sur infra-pki-01 — l'autorite elle-meme — le certificat etait expire depuis plus de 8 h et le renouvellement echouait toutes les 14 minutes sur « 'step ca renew' requires the '--ca-url' flag ». Rien ne le signalait. Cause : sur l'hote de l'AC, /etc/step est le STEPPATH du SERVEUR, pas un amorcage client — pas de defaults.json, et l'unite de renouvellement en dependait. La lecon etait deja ecrite dans le commentaire de la tache d'emission (« l'autorite ne bootstrape pas »), jamais reportee sur l'unite. Le role ne pouvait pas non plus se soigner : la re-emission ne regardait que la FORME (cert absent ou SAN manquant), jamais la validite. client_pki verifie desormais l'echeance (client_pki_marge_renouvellement). Ce qu'il a fallu desapprendre : les certificats vivent 24 h et se renouvellent toutes les ~14 min ; « empreinte servie != empreinte disque » est l'etat NORMAL. Comparer les empreintes aurait donne un verificateur qui crie en permanence. Le signal est l'echeance de ce qui est SERVI, plus l'absence de client_pki_reload_services. Le devis a d'abord menti, du defaut meme qu'il traque : include_vars au niveau du play prime sur les group_vars. Et le premier correctif a PARU marcher — set_fact accepte un dictionnaire entier en argument libre sans erreur et n'en fait rien. Il faut reimposer cle par cle. Les deux devis sont corriges et le piege est consigne dans docs/devis-services.md avant d'ecrire le prochain. Verifie dans les deux sens : CONFORME sur 14 hotes ; sur un releve ou l'on rejoue une copie perimee en memoire, 2 ecarts et code de sortie 1. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 07:20:56 -04:00
certificats-plan: ansible-runtime ## Ecart entre les certificats sur disque et ceux reellement servis (aucune ecriture)
@rm -f $(SETOPS_INSTANCE)/devis-certificats.json.*
@ansible-playbook -i $(SETOPS_INVENTAIRE) playbooks/maintenance/devis-certificats.yml >/dev/null
@python3 scripts/devis_certificats.py
ports-verifier: ## Deux roles co-localises revendiquent-ils le meme port ? (lecture seule)
@python3 scripts/verifier_ports.py
intrants-verifier: ## Tout intrant qu'un role EXIGE est-il fourni par l'instance ? (lecture seule)
@python3 scripts/verifier_intrants.py
identite-plan: ansible-runtime ## Ecart entre l'identite deployee et ce que le plan derive (aucune ecriture)
@ansible-playbook -i $(SETOPS_INVENTAIRE) playbooks/maintenance/devis-identite.yml >/dev/null
@python3 scripts/devis_identite.py
frontiere-plan: ansible-runtime ## Ecart entre la frontiere OPNsense et son devis (aucune ecriture)
python3 scripts/appliquer_opnsense.py
frontiere-appliquer: ansible-runtime ## Reconcilie la frontiere : cree ce qui manque, RETIRE ce qui est perime. CONFIRMER=true
@if [[ "$(CONFIRMER)" != "true" ]]; then \
printf '%s\n' "Refus: cette cible ECRIT sur le pare-feu de bordure et RETIRE les regles perimees." \
"Relire d'abord 'make frontiere-plan', puis: make frontiere-appliquer CONFIRMER=true"; \
exit 2; fi
CONFIRMER=true python3 scripts/appliquer_opnsense.py
.PHONY: devis-proxmox-fw devis-proxmox-fw-verifier
devis-proxmox-fw: ansible-runtime ## Devis pare-feu Proxmox (est-ouest intra-tenant), derive du registre des flux
python3 scripts/devis_proxmox_fw.py $(if $(JSON),--json,)
devis-proxmox-fw-verifier: ## Verifie le devis du pare-feu est-ouest Proxmox (aucune ecriture)
python3 scripts/devis_proxmox_fw.py --verifier
intrants du site : ce qu un locataire doit savoir pour l habiter, derive Un locataire ECRIT les adresses des services de son site — resolveur, cache, forge, depot de sauvegarde, plan d administration, sortie. Une copie se perime, et deux l avaient fait en deux jours avec la meme forme : `serveur_ops_forge_amont` visait 10.0.33.11 quand la forge sert en 10.37.33.11, et `ac-racine-site.crt` portait la racine d avant la reconstruction du site. Rien ne les relisait. `make site-intrants` lit le plan du site et rend le contrat — sept valeurs, toutes derivees. `make site-intrants-verifier` les confronte a ce que le locataire declare, et P73 en fait une preuve. Elle a trouve le defaut de la forge des sa premiere execution. Le contrat se DERIVE du plan du site, pas d une liste tenue a part : ajouter un service prete au site l ajoute au contrat, sans qu on ait a y penser. P69 RESTREINTE AU COUPLE MONTE. Elle balayait tous les depots OPS-* et les comparait au site monte. Elle avait raison tant qu un seul site existait : une adresse en 10.x.3z ne pouvait designer que lui. Deux sites decoupent leurs zones de la meme facon — c est le but, un locataire doit pouvoir habiter l un ou l autre sans se renumeroter. Le troisieme octet a cesse de distinguer « mon site » d « un autre site » : 10.31.34.11, juste pour un locataire de TechnoLibre, etait declare faux parce que Chezlepro etait monte. Un locataire n appartient a aucun site — il en habite un, choisi par le symlink au deploiement. La seule paire jugeable est celle qui est montee. Meme portee que P73. Une marche payee : la premiere version de site_intrants recopiait la resolution d instance au lieu de la partager. P41 a mordu — neuf modules avaient deja porte chacun leur copie, et cinq defauts en etaient sortis en cinq jours. make prouver : 72 OK, 0 echec, 1 saute. ansible-lint : 0 failure. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-13 16:18:12 -04:00
.PHONY: site-intrants site-intrants-verifier
site-intrants: ## Les intrants que le SITE monte expose a ses locataires (derives)
python3 scripts/site_intrants.py $(if $(JSON),--json,)
site-intrants-verifier: ## Le locataire monte suit-il encore son site ? (aucune ecriture)
python3 scripts/site_intrants.py --verifier
pools Proxmox : un par tenant, dérivé de l'index (D-37, P28) Onze des quatorze serveurs portent le même nom court chez Chezlepro et chez Technolibre. Vérifié un par un, ce n'est pas un problème technique : tout le reste dérive du seed et diverge (10.27.19.21 contre 10.21.19.21, VMID 117402101 contre 111402101, VLAN 1174 contre 1114, deux domaines internes), et rien n'est indexé sur le nom court — les opérations Proxmox portent toutes un vmid, les certificats un FQDN, et client_backup_repo vise backup-01.{{ domaine_interne }}. Le coût est humain : la console Proxmox affiche le nom, et deux infra-pki-01 y sont indiscernables à l'œil. Le VMID porte le tenant, encore faut-il connaître le codage. Un pool par tenant, dérivé du dossier d'instance et de l'index — déjà unique par P21, donc aucun registre de plus : Chezlepro-17, Technolibre-11. make devis-proxmox-pools rattrape la flotte existante (création du pool, puis affectation des VM actives). Les VM créées ensuite entrent d'elles-mêmes : make creer-vm dérive le pool par la même fonction et le passe à la création. Le playbook crée le pool au préalable — proxmox_kvm échoue sur un pool inconnu, et l'API ne sait pas changer le pool d'une VM existante ; c'est aussi pourquoi le rattrapage passe par les membres. P28 garde deux collisions : même nom de pool entre tenants, et surtout même VMID — une machine appartenant à deux tenants serait pire qu'une homonymie. Rien n'est renommé : les homonymes sont la preuve que la nomenclature est un vrai gabarit. Le devis ne lit pas le cluster, il dit l'état cible et non l'écart. 27 preuves OK, 0 échec. --syntax-check du playbook de clonage. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 15:15:14 -04:00
.PHONY: devis-proxmox-pools devis-proxmox-pools-verifier
devis-proxmox-pools: ansible-runtime ## Devis des pools Proxmox (un par tenant), derive du plan
python3 scripts/devis_proxmox_pools.py $(if $(JSON),--json,)
devis-proxmox-pools-verifier: ## Verifie le devis des pools Proxmox (aucune ecriture)
pools Proxmox : un par tenant, dérivé de l'index (D-37, P28) Onze des quatorze serveurs portent le même nom court chez Chezlepro et chez Technolibre. Vérifié un par un, ce n'est pas un problème technique : tout le reste dérive du seed et diverge (10.27.19.21 contre 10.21.19.21, VMID 117402101 contre 111402101, VLAN 1174 contre 1114, deux domaines internes), et rien n'est indexé sur le nom court — les opérations Proxmox portent toutes un vmid, les certificats un FQDN, et client_backup_repo vise backup-01.{{ domaine_interne }}. Le coût est humain : la console Proxmox affiche le nom, et deux infra-pki-01 y sont indiscernables à l'œil. Le VMID porte le tenant, encore faut-il connaître le codage. Un pool par tenant, dérivé du dossier d'instance et de l'index — déjà unique par P21, donc aucun registre de plus : Chezlepro-17, Technolibre-11. make devis-proxmox-pools rattrape la flotte existante (création du pool, puis affectation des VM actives). Les VM créées ensuite entrent d'elles-mêmes : make creer-vm dérive le pool par la même fonction et le passe à la création. Le playbook crée le pool au préalable — proxmox_kvm échoue sur un pool inconnu, et l'API ne sait pas changer le pool d'une VM existante ; c'est aussi pourquoi le rattrapage passe par les membres. P28 garde deux collisions : même nom de pool entre tenants, et surtout même VMID — une machine appartenant à deux tenants serait pire qu'une homonymie. Rien n'est renommé : les homonymes sont la preuve que la nomenclature est un vrai gabarit. Le devis ne lit pas le cluster, il dit l'état cible et non l'écart. 27 preuves OK, 0 échec. --syntax-check du playbook de clonage. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 15:15:14 -04:00
python3 scripts/devis_proxmox_pools.py --verifier
SDN : make devis-sdn — zone, VNets et sous-réseaux dérivés du seed (D-43/44, P30) Ajouter un tenant implique 1 zone EVPN + 6 VNets + 6 sous-réseaux sur le cluster. Aucun générateur ne les produisait : dernière lacune dans un dépôt où tout dérive. 26 objets pour les deux tenants, VNI = 1000 + index×10 + zone, sous-réseau et passerelle par les mêmes fonctions que l'inventaire. Nommage, en deux temps. D'abord VRF0017 / v1174, alignés sur ce que le cluster portait — réflexe inverse du bon : cette convention venait d'une création à la main et ne disait pas de quel tenant il s'agissait. Forme retenue : t<index> pour la zone, t<index><zone abrégée> pour le VNet (t17, t17serv). C'est le préfixe que le pare-feu Proxmox utilisait déjà (t17-cli-metrique), donc un seul schéma dans tout le dépôt. Abréviation = 4 premières lettres du libellé, accents retirés. Pas de tiret entre index et zone, contrairement aux IPSets : t245-serv ferait 9 caractères, t245serv en fait 8 — la forme reste uniforme jusqu'au dernier index. Contrainte cadrante : zones ET VNets sont limités à 8 caractères par Proxmox. sdn-evpn.md annonçait chez17-services-infra (21) : il aurait été refusé à l'application. P30 refuse tout dépassement et toute collision de nom, de VNI ou de sous-réseau. Éprouvé aux bornes et par sabotage. Vérification la plus forte : avant renommage, la dérivation reproduisait à l'identique les deux zones créées à la main — nom, VNI de VRF, MTU, contrôleur. voute.py saisir : le pendant de la génération. On génère un secret dont le dépôt est la source, on saisit celui dont un tiers est la source — inventer une clé d'API OPNsense donnerait une valeur refusée à la première requête, avec P18 au vert sur une voûte inutilisable. Sans écho, double confirmation, rien sur la ligne de commande. Reste ouvert : aucun nœud de sortie déclaré. Le devis émet un marqueur, pas une valeur plausible. Deux points à trancher — le nœud de sortie route selon sa propre table (défaut actuel : 192.168.11.254, pas la frontière), et l'entrée n'est pas redondante puisqu'elle dépend d'une route statique d'OPNsense vers un seul nœud. D-45 : l'affinité de VM attend Proxmox 9 (cluster en 8.4.19), tenue à la main. 30 preuves OK. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 11:06:42 -04:00
.PHONY: devis-sdn devis-sdn-verifier
devis-sdn: ansible-runtime ## Devis SDN EVPN (zone + VNets + sous-reseaux par tenant), derive du seed
python3 scripts/devis_sdn.py $(if $(JSON),--json,)
devis-sdn-verifier: ## Verifie le devis SDN EVPN — zones, VNets, sous-reseaux (aucune ecriture)
SDN : make devis-sdn — zone, VNets et sous-réseaux dérivés du seed (D-43/44, P30) Ajouter un tenant implique 1 zone EVPN + 6 VNets + 6 sous-réseaux sur le cluster. Aucun générateur ne les produisait : dernière lacune dans un dépôt où tout dérive. 26 objets pour les deux tenants, VNI = 1000 + index×10 + zone, sous-réseau et passerelle par les mêmes fonctions que l'inventaire. Nommage, en deux temps. D'abord VRF0017 / v1174, alignés sur ce que le cluster portait — réflexe inverse du bon : cette convention venait d'une création à la main et ne disait pas de quel tenant il s'agissait. Forme retenue : t<index> pour la zone, t<index><zone abrégée> pour le VNet (t17, t17serv). C'est le préfixe que le pare-feu Proxmox utilisait déjà (t17-cli-metrique), donc un seul schéma dans tout le dépôt. Abréviation = 4 premières lettres du libellé, accents retirés. Pas de tiret entre index et zone, contrairement aux IPSets : t245-serv ferait 9 caractères, t245serv en fait 8 — la forme reste uniforme jusqu'au dernier index. Contrainte cadrante : zones ET VNets sont limités à 8 caractères par Proxmox. sdn-evpn.md annonçait chez17-services-infra (21) : il aurait été refusé à l'application. P30 refuse tout dépassement et toute collision de nom, de VNI ou de sous-réseau. Éprouvé aux bornes et par sabotage. Vérification la plus forte : avant renommage, la dérivation reproduisait à l'identique les deux zones créées à la main — nom, VNI de VRF, MTU, contrôleur. voute.py saisir : le pendant de la génération. On génère un secret dont le dépôt est la source, on saisit celui dont un tiers est la source — inventer une clé d'API OPNsense donnerait une valeur refusée à la première requête, avec P18 au vert sur une voûte inutilisable. Sans écho, double confirmation, rien sur la ligne de commande. Reste ouvert : aucun nœud de sortie déclaré. Le devis émet un marqueur, pas une valeur plausible. Deux points à trancher — le nœud de sortie route selon sa propre table (défaut actuel : 192.168.11.254, pas la frontière), et l'entrée n'est pas redondante puisqu'elle dépend d'une route statique d'OPNsense vers un seul nœud. D-45 : l'affinité de VM attend Proxmox 9 (cluster en 8.4.19), tenue à la main. 30 preuves OK. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 11:06:42 -04:00
python3 scripts/devis_sdn.py --verifier
devis-opnsense-verifier: ## Verifie le devis de la frontiere nord/sud (aucune ecriture)
frontière nord/sud : devis dérivé, lien de transit et les deux routes La bordure devient un artefact dérivé, comme le devis switch — et le chemin qui y mène est enfin déclaré. `make devis-opnsense` (+ preuve P24) dérive la politique de bordure du registre des flux : les flux `pair: externe`, que `resoudre_flux.py` saute volontairement parce qu'ils relèvent de la frontière et non du pare-feu d'hôte. Aucun port, aucune adresse, aucun nom d'hôte dans le générateur. Le lien manquait dans tous les fichiers : le devis switch ne contenait pas une seule `ip route`. Un réseau underlay portant `passerelle_sortie` le déclare — il vit dans l'underlay et non dans un tenant parce que la frontière route vers TOUS les supernets tenants par le même saut, donc il ne peut dériver d'aucun `index`. `devis-reseau` en tire deux routes : l'aller (sortie générale) et le retour vers l'administration, dont l'absence a coûté la passe de déploiement du 2026-07-29 — la réponse revient au pare-feu par une autre interface que celle où l'état a été créé, et se fait jeter en silence. Les réseaux d'administration viennent de l'intrant `nftables_admin_ssh` : même source unique que la garde anti-lockout des nftables et l'alias SETOPS_ADMIN. Les trois pare-feux et les routes ne peuvent plus diverger. La frontière est réglable depuis la console (section « Frontière » du panneau Intrants) ; les identifiants d'API restent interdits d'écriture par le GUI et vivent dans la voûte. Correctifs de la même passe : - le panneau refusait d'enregistrer les intrants de la frontière : le garde-fou confondait une référence de voûte `{{ vault_* }}` préservée avec un secret soumis. Il regarde désormais la valeur, pas le nom. - `supprimer_vm_debian.yml` ne chargeait que `proxmox.vault.yml` pour ses secrets ; retirer ce reliquat aurait cassé `make detruire`. Aligné sur le playbook de clonage, voûte unique en dernier. - documentation : la voûte est unique, `proxmox.vault.yml` n'est qu'un reliquat de compatibilité. Preuves : 24 OK, 0 échec. Cas de rejet du validateur d'underlay exercés un par un ; résolution du jeton Proxmox vérifiée en exécution réelle. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:32:04 -04:00
python3 scripts/devis_opnsense.py --verifier
.PHONY: underlay
underlay: ## Underlay (fabric physique cluster-global : mgmt/iSCSI/Ceph) : affiche + valide (P23)
python3 scripts/underlay.py
underlay : la frontiere entre les deux mondes, et l'instrument qui la mesure Demande de l'exploitant : « il faut vraiment faire une distinction entre l'underlay et les tenants. Definis bien la frontiere entre les deux mondes. L'underlay et son tenant doivent avoir chacun sa voute. » LA DOCTRINE (docs/frontiere-physique-virtuel.md) : qui possede quoi, ou ca vit, qui l'administre. Et la regle qui la rend operante — UN TENANT NE DETIENT JAMAIS UN SECRET DU MONDE PHYSIQUE. Aujourd'hui chaque tenant porte le jeton d'API du cluster ; patient 0 a du le recopier pour exister. C'est la faute des neuf copies, appliquee aux secrets : une valeur qui vit a N endroits diverge, et on ne peut plus en revoquer une sans les autres. L'INSTRUMENT (`make underlay-plan`) confronte le fichier au reel : l'API du cluster pour les adresses REELLEMENT portees, une sonde TCP pour ce qui repond. N'ecrit rien. CE QU'IL A TROUVE, des le premier passage : management 10.0.0.0/24, stockage 10.0.1.0/24, ceph 10.0.2-3.0/24 -> PERSONNE transit 10.0.4.0/24, vxlan 10.0.5.0/24 -> occupes (3 noeuds) portes par les noeuds et declares NULLE PART : 192.168.11.x (la vraie gestion), 10.11.5-7.x, 192.168.50.x, 10.1.110.254 La frontiere avait deja migre vers 10.17.0.1 ; les hyperviseurs, non. Le fichier decrivait le monde d'avant — et c'est pour cela que la regle d'admin de patient 0 atterrissait sur `wan`, ou elle n'aurait jamais laisse passer personne. DEUX PRECAUTIONS ECRITES DANS L'INSTRUMENT, apprises en l'ecrivant : - l'AUTORITE DEPEND DU ROLE. L'API de Proxmox connait ses hyperviseurs, et eux seuls. Declarer un commutateur « porte par personne » parce que le cluster l'ignore, c'est accuser le monde de ce que l'instrument ne voit pas. - « pas joignable d'ici » n'est pas « absent ». Les reseaux de CHEMIN (transit, VXLAN, stockage) ne sont jamais joignables de l'exterieur, par construction (D-78). Le premier jet les declarait morts. Le devis a donc corrige DEUX FOIS sa propre facon de mesurer avant de rendre un verdict. RESTE, dans l'ordre : ecrire dans underlay.yml ce qui EST ; separer les voutes ; et alors seulement appliquer la frontiere. make verifier 41/41. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 16:31:35 -04:00
underlay-plan: ## Confronte l'underlay DECLARE au reel (API du cluster + sonde TCP) — n'ecrit rien
python3 scripts/devis_underlay.py
flux-verifier: ## Verifie que le registre des flux correspond aux meta/flux.yml des roles
python3 scripts/resoudre_flux.py verifier
.PHONY: valider
valider: ansible-runtime ## Passe la recette de validation sur la flotte
ansible-playbook -i $(INVENTAIRE_PRODUCTION) playbooks/valider.yml
schema du plan : la forme des registres devient derivee, et gardee Etape 2 du chantier « l UI reflete fidelement la structure ». Le GUI porte CHAMPS_ECRITS_PAR_GUI, une liste tenue A LA MAIN de ce qu il sait ecrire, que P19 confronte au reel. C est une copie — gardee, donc honnete, mais une copie : quelqu un doit penser a l allonger. `make schema` produit docs/audit/schema-plan.json : six registres, 42 champs, leurs types, leurs enumerations et ce qui est requis. CE QUE LE SCHEMA EST, ET CE QU IL N EST PAS schema -> la FORME -> generera les champs du formulaire validateurs -> la COHERENCE -> refusent une saisie incoherente Un JSON Schema ne sait pas dire qu un `consommateur` designe une application inexistante, ni qu une integration universelle recopiee au plan est un defaut. Vouloir l y mettre creerait la seconde source de verite que tout ce depot refuse. Les valider_* restent l autorite. LES ENUMERATIONS SONT IMPORTEES, JAMAIS RECOPIEES ETATS_SERVEUR, PORTEES_BD et AUTORITES_DNS viennent des constantes que les validateurs appliquent. Une enumeration recopiee diverge — c est la lecon des neuf resolutions d instance que P41 garde depuis. CE QUE L ETAPE 1 AVAIT TROUVE, ET QUE P61 A CONFIRME Le recensement montrait `categorie` et `service` presents dans TOUS les plans et absents de CHAMPS_ECRITS_PAR_GUI, dont la ligne `nomenclature` est vide : le GUI ne sait pas les editer, l operateur doit ouvrir le YAML. P61 a refuse le premier schema pour cette raison exacte. Les trois tables imbriquees de la nomenclature sont donc DECRITES et non resumees en « object ». 31 champs observes dans l instance courante, 42 decrits par le schema. La difference n est pas du bavardage : observer une instance n est pas un schema. `noeud`, `stockage` et `coeurs` sont legitimes et simplement inutilises ici — un schema derive de l observation les INTERDIRAIT. P61, EPROUVEE DANS LES DEUX SENS fichier genere perime -> REFUSE champ du plan absent du schema -> REFUSE champ decrit mais inutilise au plan -> COMPTE, pas refuse Le troisieme point est delibere : refuser obligerait a retirer du schema un champ valide des que plus personne ne s en sert. Meme mesure que les lacunes nommees de P29. UN DEFAUT DE MON INSTRUMENT, PAYE EN ROUTE P61 comparait des noms a plat quand couverture_gui aplatit les tables imbriquees : elle criait sur un schema correct. L instrument mesurait autre chose que la cible. On aplatit desormais des deux cotes. make prouver : CONFORME, 60 OK, 0 echec, 1 saute. Prochaine etape : generer les formulaires depuis ce schema, et retirer CHAMPS_ECRITS_PAR_GUI. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 15:19:20 -04:00
.PHONY: schema
schema: ## Regenere docs/audit/schema-plan.json depuis les registres et les validateurs
python3 scripts/schema_plan.py
.PHONY: wiki-publier
wiki-publier: ## Publie le wiki (wiki/) vers la forge
@set -e; \
if [[ -z "$(WIKI_REMOTE)" ]]; then \
printf '%s\n' 'Refus: URL du wiki Forgejo requise.'; \
printf '%s\n' 'Ex: make wiki-publier WIKI_REMOTE=https://forge.<domaine>/<proprio>/<depot>.wiki.git'; \
exit 2; \
fi; \
src="$(CURDIR)/wiki"; \
tmp="$$(mktemp -d)"; \
trap 'rm -rf "$$tmp"' EXIT; \
printf '%s\n' "Clonage du wiki: $(WIKI_REMOTE)"; \
if ! git clone --quiet --depth 1 "$(WIKI_REMOTE)" "$$tmp/wiki"; then \
printf '%s\n' 'Echec du clone (URL ou acces ?). Le wiki doit exister (creer une 1re page dans Forgejo).'; \
exit 1; \
fi; \
wiki-publier : un wiki vide se clone tres bien, et le garde-fou ne le voyait pas En rattrapant origin, sa forge s est revelee porter un wiki VIDE — jamais publie. Avant de viser la forge, la manoeuvre a ete eprouvee contre un depot bare local, comme la premiere fois. LE DEFAUT Le garde-fou ne testait que l ECHEC du clone, alors que son message parle du cas vide : « le wiki doit exister (creer une 1re page dans Forgejo) ». Or un depot vide se clone parfaitement. La recette allait donc au bout et creait une branche `master` — parce que init.defaultBranch vaut master sur ce poste — alors que le wiki deja en service vit sur `main`. Le nom de branche etait DEVINE, et il divergeait d une forge a l autre. Un wiki publie sur la mauvaise branche est un wiki que Forgejo peut ne pas afficher : la publication reussit, la page reste introuvable, et rien ne l explique. LE CORRECTIF Refuser un wiki sans commit, plutot que de choisir a la place de Forgejo. Creer une premiere page dans son interface initialise le wiki avec LA branche qu il attend — il n y a plus rien a deviner. EPROUVE DANS LES DEUX SENS depot bare vide -> REFUSE, avec le geste a faire depot bare non vide -> publie forge eregion reelle -> « deja a jour », temoin depose Le troisieme essai compte autant que le premier : remplacer un defaut par un blocage aurait ete un autre defaut. CE QUE CA LAISSE OUVERT Le wiki d origin reste vide : l initialiser demande une action humaine dans l interface de la forge, que cette cible refuse desormais de contourner. Et le temoin ne nomme qu UNE forge. P60 prouve que le depot n a pas bouge depuis la derniere publication — pas que toutes les forges sont a jour. Tant qu il n y en a qu une de publiee, la distinction ne coute rien ; a deux, il faudra que le temoin porte une liste. make prouver : CONFORME, 59 OK, 0 echec, 1 saute. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 12:56:21 -04:00
: ' UN WIKI VIDE SE CLONE TRES BIEN, et le garde-fou ci-dessus ne le voyait pas :'; \
: ' il ne teste que l ECHEC du clone, alors que son message parle du cas VIDE.'; \
: ' Consequence mesuree le 2026-09-08 contre un depot bare local : la recette'; \
: ' allait au bout et creait une branche `master` — parce que `init.defaultBranch`'; \
: ' vaut master sur ce poste — alors que le wiki deja publie vit sur `main`. Le nom'; \
: ' de branche etait DEVINE, et il divergeait d une forge a l autre.'; \
: ' On refuse donc plutot que de choisir a la place de Forgejo : creer une premiere'; \
: ' page dans son interface initialise le wiki avec LA branche qu il attend.'; \
: ' ON NE DEVINE PAS — ON DEMANDE. Forgejo DECLARE la branche qu il attend, dans'; \
: ' `wiki_branch` de son API : GET /api/v1/repos/<proprio>/<depot>. Mesure du'; \
: ' 2026-09-09 sur forge.genese.internal : `wiki_branch = main`. Le refus ci-dessous'; \
: ' reste le DEFAUT — depuis ce poste l API n est pas joignable (la forge du site'; \
: ' n accepte le 443 que des machines qui declarent le flux) — mais qui a la reponse'; \
: ' peut la donner, au lieu de passer par l interface web :'; \
: ' make wiki-publier WIKI_REMOTE=... WIKI_BRANCHE=main'; \
: ' C est la difference entre une garde qui protege et une garde qui bloque.'; \
wiki-publier : un wiki vide se clone tres bien, et le garde-fou ne le voyait pas En rattrapant origin, sa forge s est revelee porter un wiki VIDE — jamais publie. Avant de viser la forge, la manoeuvre a ete eprouvee contre un depot bare local, comme la premiere fois. LE DEFAUT Le garde-fou ne testait que l ECHEC du clone, alors que son message parle du cas vide : « le wiki doit exister (creer une 1re page dans Forgejo) ». Or un depot vide se clone parfaitement. La recette allait donc au bout et creait une branche `master` — parce que init.defaultBranch vaut master sur ce poste — alors que le wiki deja en service vit sur `main`. Le nom de branche etait DEVINE, et il divergeait d une forge a l autre. Un wiki publie sur la mauvaise branche est un wiki que Forgejo peut ne pas afficher : la publication reussit, la page reste introuvable, et rien ne l explique. LE CORRECTIF Refuser un wiki sans commit, plutot que de choisir a la place de Forgejo. Creer une premiere page dans son interface initialise le wiki avec LA branche qu il attend — il n y a plus rien a deviner. EPROUVE DANS LES DEUX SENS depot bare vide -> REFUSE, avec le geste a faire depot bare non vide -> publie forge eregion reelle -> « deja a jour », temoin depose Le troisieme essai compte autant que le premier : remplacer un defaut par un blocage aurait ete un autre defaut. CE QUE CA LAISSE OUVERT Le wiki d origin reste vide : l initialiser demande une action humaine dans l interface de la forge, que cette cible refuse desormais de contourner. Et le temoin ne nomme qu UNE forge. P60 prouve que le depot n a pas bouge depuis la derniere publication — pas que toutes les forges sont a jour. Tant qu il n y en a qu une de publiee, la distinction ne coute rien ; a deux, il faudra que le temoin porte une liste. make prouver : CONFORME, 59 OK, 0 echec, 1 saute. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 12:56:21 -04:00
if ! git -C "$$tmp/wiki" rev-parse --verify --quiet HEAD >/dev/null; then \
if [[ -n "$(WIKI_BRANCHE)" ]]; then \
printf '%s\n' "Wiki vide : initialisation sur la branche declaree « $(WIKI_BRANCHE) »."; \
git -C "$$tmp/wiki" checkout -q -b "$(WIKI_BRANCHE)"; \
else \
printf '%s\n' 'Refus: ce wiki est VIDE (aucun commit).' \
'Publier ici inventerait un nom de branche — sur ce poste `master`, alors que' \
'le wiki deja en service vit sur `main`. Deux issues :' \
' - demander a Forgejo : GET /api/v1/repos/<proprio>/<depot> -> `wiki_branch`,' \
' puis make wiki-publier WIKI_REMOTE=... WIKI_BRANCHE=<cette valeur>' \
" - ou creer une premiere page dans l interface Forgejo de $(WIKI_REMOTE)"; \
exit 2; \
fi; \
wiki-publier : un wiki vide se clone tres bien, et le garde-fou ne le voyait pas En rattrapant origin, sa forge s est revelee porter un wiki VIDE — jamais publie. Avant de viser la forge, la manoeuvre a ete eprouvee contre un depot bare local, comme la premiere fois. LE DEFAUT Le garde-fou ne testait que l ECHEC du clone, alors que son message parle du cas vide : « le wiki doit exister (creer une 1re page dans Forgejo) ». Or un depot vide se clone parfaitement. La recette allait donc au bout et creait une branche `master` — parce que init.defaultBranch vaut master sur ce poste — alors que le wiki deja en service vit sur `main`. Le nom de branche etait DEVINE, et il divergeait d une forge a l autre. Un wiki publie sur la mauvaise branche est un wiki que Forgejo peut ne pas afficher : la publication reussit, la page reste introuvable, et rien ne l explique. LE CORRECTIF Refuser un wiki sans commit, plutot que de choisir a la place de Forgejo. Creer une premiere page dans son interface initialise le wiki avec LA branche qu il attend — il n y a plus rien a deviner. EPROUVE DANS LES DEUX SENS depot bare vide -> REFUSE, avec le geste a faire depot bare non vide -> publie forge eregion reelle -> « deja a jour », temoin depose Le troisieme essai compte autant que le premier : remplacer un defaut par un blocage aurait ete un autre defaut. CE QUE CA LAISSE OUVERT Le wiki d origin reste vide : l initialiser demande une action humaine dans l interface de la forge, que cette cible refuse desormais de contourner. Et le temoin ne nomme qu UNE forge. P60 prouve que le depot n a pas bouge depuis la derniere publication — pas que toutes les forges sont a jour. Tant qu il n y en a qu une de publiee, la distinction ne coute rien ; a deux, il faudra que le temoin porte une liste. make prouver : CONFORME, 59 OK, 0 echec, 1 saute. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 12:56:21 -04:00
fi; \
find "$$tmp/wiki" -maxdepth 1 -name '*.md' -delete; \
for f in "$$src"/*.md; do \
bn="$$(basename "$$f")"; \
[[ "$$bn" == "README.md" ]] && continue; \
cp "$$f" "$$tmp/wiki/$$bn"; \
done; \
rm -rf "$$tmp/wiki/img"; \
if compgen -G "$$src/img/*-annote.svg" > /dev/null; then \
mkdir -p "$$tmp/wiki/img"; \
cp -L "$$src"/img/*-annote.svg "$$tmp/wiki/img/"; \
fi; \
cd "$$tmp/wiki"; \
2026-09-08 12:49:40 -04:00
sha="$$(git -C "$(CURDIR)" rev-parse --short HEAD 2>/dev/null || echo inconnu)"; \
if [[ -z "$$(git status --porcelain)" ]]; then \
printf '%s\n' 'Wiki deja a jour (aucun changement).'; \
2026-09-08 12:49:40 -04:00
else \
git add -A; \
git commit --quiet -m "Publication du wiki depuis le depot (source: $$sha)"; \
git push --quiet; \
printf '%s\n' 'Wiki publie.'; \
fi; \
2026-09-08 12:49:40 -04:00
: ' LE TEMOIN. Sans lui, l ecart entre wiki/ et la forge n est mesurable par'; \
: ' personne : il s est creuse de 27 jours en silence avant que P60 n existe.'; \
: ' ECRIT DANS LES DEUX BRANCHES, et la seconde est la moins evidente. « Deja a'; \
: ' jour » est precisement le cas ou le temoin doit dire que la forge est au'; \
: ' niveau du depot : sortir la sans rien deposer laissait P60 rouge apres une'; \
: ' publication reussie, ce qui apprend a ignorer la preuve. Paye une fois.'; \
: ' APRES le push : un temoin ne doit jamais dire qu on a publie si on a echoue.'; \
preuves : trois gardes pour ce que ma lecture ne tiendra pas Une revision de documentation vieillit comme le reste. Ce qui tient, c est ce qu une machine verifie — et trois lacunes etaient nommees sans etre gardees. P58 HABILITATIONS. autorisation.md posait la regle (un service nomme un GROUPE, jamais une personne, D-66) et meta/acces.yml la portait ; rien ne la verifiait. P29 gardait les POSITIONS d authentification, personne ne gardait les DROITS. Le controle qui porte la preuve est un croisement : une entree porte_par: role-realm affirme que l habilitation voyage par un role de realm projete depuis un groupe LDAP. P58 le confronte a serveur_keycloak. Sans ca, un service annonce une habilitation que rien ne transporte, et l ecran reste vide sans que personne sache pourquoi. CE QU ELLE N EXIGE PAS, et c est le point le plus important : que les groupes nommes existent dans l annuaire. Ce serait contredire le regime du paragraphe 2 — le depot AMORCE un acces et se retire, les appartenances appartiennent a une personne. dev et personnel n existent dans aucun code, et ce n est pas un defaut. P59 ENUMERATIONS ANNONCEES. Les deux ecarts trouves a la main pendant la tournee — cinq portes annoncees devant une table de six, huit lignes renvoyees vers une fiche qui en compte dix — etaient d une forme que P57 ne voit pas. Ma premiere version a signale CINQ ecarts, et les cinq etaient du bruit : dans « reprise dans les deux devis : », le nombre qualifie autre chose que la liste. Cent pour cent de faux positifs — la preuve qui crie sur un cas sain et qu on apprend a ignorer. Resserree aux deux formes ou le nombre ne peut compter rien d autre. Etroite et vraie plutot que large et devineuse. P60 WIKI PUBLIE. Le wiki est publie DEPUIS le depot ; rien ne mesurait l ecart, et il s est creuse de VINGT-SEPT JOURS en silence. Deux unites jamais publiees, vingt et une differentes : pour qui lit la forge plutot que le depot, toute la revision n existait pas. Le harnais est STATIQUE, zero appel reseau — cloner la forge romprait la seule propriete qui fasse qu une preuve vaille hors de ce poste. La mesure passe donc par un TEMOIN que make wiki-publier depose. Amorce avec la valeur MESUREE : le wiki d eregion porte b6167f2, dont le message dit source: ac85278. Ce qu elle ne prouve pas : un temoin dit ce qui est PARTI, jamais ce qui est ARRIVE. LES TROIS SONT EPROUVEES DANS LES DEUX SENS Douze essais negatifs, douze refus : groupe non projete, acces.yml disparu, personne au lieu d un groupe, mecanisme invente, raison manquante, compte revenu a cinq, septieme porte ajoutee sans toucher au compte, renvoi croise fausse, temoin absent, temoin d un autre depot. Une garantie qu on n a jamais vu dire non n est pas une garantie, c est une habitude. ETAT : NON CONFORME, 58 OK, 1 echec, 1 saute. P60 est rouge, et c est le comportement voulu : le registre a le droit de perdre. Le retard qu elle signale est reel et anterieur a elle. Une commande le ferme, et elle vient ensuite. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 12:47:18 -04:00
{ printf '%s\n' '---' \
'# Ecrit par `make wiki-publier`, lu par la preuve P60. Ne pas editer a la main.' \
"remote: $(WIKI_REMOTE)" \
"source: $$sha" \
"date: $$(date +%F)"; } > "$(CURDIR)/docs/audit/wiki-publie.yml"; \
printf '%s\n' 'Temoin depose : docs/audit/wiki-publie.yml (a committer).'
deployer-tout: _instance-requise ## Deploie TOUTE la flotte dans l'ordre des couches
@set -e; \
if [[ "$(CONFIRMER)" != "true" ]]; then \
printf '%s\n' 'Refus: deploiement ORCHESTRE de TOUTE la flotte (action impactante).'; \
printf '%s\n' 'Relancer avec CONFIRMER=true. Astuce: tester d abord en idempotent avec MODE_CHECK=1.'; \
exit 2; \
fi; \
python3 scripts/orchestrer.py verifier; \
python3 scripts/orchestrer.py ecrire; \
vault_chiffre="$$(grep -rlsIF '$$ANSIBLE_VAULT' $(dir $(INVENTAIRE_PRODUCTION))group_vars 2>/dev/null | head -1 || true)"; \
voutes : une voute, une cle — separer avant de distribuer Decision de l'exploitant : chaque runner est maitre de sa voute et en detient la cle. C'est ce qui rend un runner autonome, donc ce qui rend l'emancipation atteignable. Elle exigeait un prealable, mesure ce matin : UN SEUL mot de passe ouvrait les SIX voutes de la flotte, celle de la fabric comprise. DISTRIBUER AVANT DE SEPARER AURAIT ETE PIRE QUE LE STATU QUO : poser « la » cle sur chaque runner rendait chaque runner capable d'ouvrir les autres. Compromettre le plus petit locataire donnait les secrets de l'hebergeur. Cinq cles, une par ecosysteme, sous ~/.config/setops-vault-<depot>. Verification croisee apres rechiffrement : la diagonale, et rien qu'elle. L'ancienne cle maitresse n'ouvre plus aucune des six. UN SEUL MOT DE PASSE NE POUVAIT PLUS SUFFIRE, et pas pour la raison qu'on croit : `cloner_vm_debian.yml` charge la voute du TENANT puis celle de l'UNDERLAY dans la meme execution. `ANSIBLE_VAULT_IDENTITY_LIST` en porte plusieurs et les essaie toutes — un seul export suffit pour les 28 appels a ansible-playbook, sans en toucher un seul. La liste se derive dans scripts/voutes.py. CE QUI BORNE LE POUVOIR N'EST PAS LA LISTE MAIS LA PRESENCE DES FICHIERS. Sur le poste, toutes les cles sont la — c'est l'humain qui les detient toutes, et P03 lit les inventaires de tous les freres. Sur un runner, une seule existe. Le code est identique, le pouvoir ne l'est pas. DEUX PREUVES ONT DIT CE QUI MANQUAIT. P03 est tombee des la separation : elle lit les inventaires voisins, donc il lui faut leurs cles — c'est elle qui a etabli que la liste devait couvrir le voisinage. P16 s'est mise a SAUTER : sa garde ne connaissait que ANSIBLE_VAULT_PASSWORD_FILE. Une preuve sautee se lit trop facilement comme une preuve passee. COUT ASSUME : le chiffre et sa cle cohabiteront sur la meme machine des que les runners recevront la leur. Le pari tient parce que le perimetre est borne. Les six voutes sauvegardees avant rechiffrement. L'ancienne cle maitresse reste sur le poste : la retirer est une decision, pas un nettoyage. make verifier : vert. make prouver : CONFORME, 52 OK, 0 echec, 0 saute — sans ANSIBLE_VAULT_PASSWORD_FILE. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-08-28 14:23:33 -04:00
if [[ -n "$$vault_chiffre" && -z "$${ANSIBLE_VAULT_PASSWORD_FILE:-}$${ANSIBLE_VAULT_IDENTITY_LIST:-}" ]]; then \
if [[ -t 0 ]]; then \
read -r -s -p 'Mot de passe du vault Ansible: ' mdp; echo; \
vf="$$(mktemp)"; printf '%s' "$$mdp" > "$$vf"; chmod 600 "$$vf"; \
export ANSIBLE_VAULT_PASSWORD_FILE="$$vf"; \
trap 'rm -f "$$vf"' EXIT; \
else \
printf '%s\n' 'Refus: vault chiffre detecte mais aucun mot de passe (entree non interactive). Fournir ANSIBLE_VAULT_PASSWORD_FILE ou le champ vault de la GUI.'; \
exit 2; \
fi; \
fi; \
$(MAKE) _verifier-acces-hote LIMITE="$(GROUPE_HOTES_ACTIFS)"; \
$(MAKE) _verifier-privileges-hote LIMITE="$(GROUPE_HOTES_ACTIFS)"; \
mode="$$([[ -n "$(MODE_CHECK)" ]] && printf -- '--check --diff' || true)"; \
ansible-playbook -i $(INVENTAIRE_PRODUCTION) playbooks/site.yml --limit "$(GROUPE_HOTES_ACTIFS)" $$mode
# --- Reconstruction from-zero : creer TOUTES les VM (2a) puis deployer (2b) ---
.PHONY: flotte-creer
perf : forks=20 et creation des VM en parallele Choisis sur le chronometrage d'une reconstruction complete (1 h 12 min 48 s, phase par phase), pas sur l'intuition. FORKS ETAIT AU DEFAUT (5) POUR 14 HOTES. Chaque play qui balaie la flotte — socle, durcissement, enrolement PKI, les cinq agents — tournait en TROIS vagues. Les gros roles n'en profitent pas : Nextcloud (10 min 55 s), Forgejo, Grafana et Keycloak sont sur UN hote. Ce sont les couches larges qui payaient. forks = 20 laisse de la marge au-dela des 14 actuels ; chaque fork est un processus sur le controleur, pas sur les cibles. LA CREATION DES VM SE FAISAIT UNE PAR UNE : 14 x 1 min 35 s = 22 min 08 s, 30 % du total, et surtout de l'ATTENTE (clone, demarrage, SSH, verrou dpkg) sans le moindre recouvrement. flotte-creer en lance quatre a la fois, ajustable par PARALLELE=n. La sortie de chaque hote va dans SON fichier, recopiee ordonnee a la fin : quatre clones ecrivant en meme temps donneraient un journal illisible, ce qui serait un comble apres une journee a traquer des diagnostics masques. Le marqueur `=== Creation VM: <hote> ===` reste emis en direct. Et un echec n'est pas avale : le code de retour de chaque hote est relu, et la cible sort en erreur si l'un a echoue — sinon _attendre-flotte partirait sur une flotte incomplete. Gains ESTIMES, a verifier par la prochaine mesure : ~15 min sur les clones, ~5 min sur les couches larges. Le troisieme levier identifie — l'archive Nextcloud en .tar.bz2, decompressee sur un seul coeur — attend d'etre mesure. Verifie : ansible-config confirme forks=20, make -n passe, prouver.py 35 OK. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 20:40:25 -04:00
# CREATION EN PARALLELE, `PARALLELE=n` pour ajuster (defaut 4).
#
# Mesure du 2026-08-10 : 14 VM creees une par une, 1 min 35 s chacune, 22 min 08 s au
# total — soit 30 % d'une reconstruction complete. Et ce temps est surtout de l'ATTENTE :
# clone, demarrage, SSH, verrou dpkg. Quatorze fois de suite, sans recouvrement.
#
# LA SORTIE DE CHAQUE HOTE VA DANS SON PROPRE FICHIER, recopiee en bloc a la fin. Quatre
# clones ecrivant en meme temps sur la meme sortie donneraient un journal illisible — et
# apres une journee passee a traquer des diagnostics masques, ce serait un comble. On
# garde donc le marqueur `=== Creation VM: <hote> ===` en direct pour suivre l'avancement,
# et le detail arrive ordonne.
#
# Un echec n'est pas avale : le code de retour de chaque hote est relu, et la cible sort
# en erreur si l'un d'eux a echoue — sinon `_attendre-flotte` partirait sur une flotte
# incomplete.
# --- LE MOT DE PASSE DE LA VOUTE, DEMANDE UNE SEULE FOIS ----------------------
#
# Deux cibles longues en ont besoin : `deployer-tout` (une invocation ansible) et
# `flotte-creer` (une invocation PAR MACHINE). La seconde redemandait le mot de passe a
# chaque VM — quinze fois sur la reconstruction de Chezlepro, quatre en parallele, avec
# la sortie redirigee vers des journaux : l'exploitant est harcele par des invites qu'il
# ne voit meme pas.
#
# UN SEUL BLOC POUR LES DEUX. Deux facons de manipuler un secret finiraient par diverger,
# et c'est le genre de divergence qu'on ne remarque qu'apres.
#
# `-t 0` : ne demander que si un humain est au clavier. Un appel non interactif refuse
# clairement plutot que de pendre sur une invite que personne ne lit.
define VAULT_UNE_FOIS
vault_chiffre="$$(grep -rlsIF '$$ANSIBLE_VAULT' $(dir $(INVENTAIRE_PRODUCTION))group_vars 2>/dev/null | head -1 || true)"; \
voutes : une voute, une cle — separer avant de distribuer Decision de l'exploitant : chaque runner est maitre de sa voute et en detient la cle. C'est ce qui rend un runner autonome, donc ce qui rend l'emancipation atteignable. Elle exigeait un prealable, mesure ce matin : UN SEUL mot de passe ouvrait les SIX voutes de la flotte, celle de la fabric comprise. DISTRIBUER AVANT DE SEPARER AURAIT ETE PIRE QUE LE STATU QUO : poser « la » cle sur chaque runner rendait chaque runner capable d'ouvrir les autres. Compromettre le plus petit locataire donnait les secrets de l'hebergeur. Cinq cles, une par ecosysteme, sous ~/.config/setops-vault-<depot>. Verification croisee apres rechiffrement : la diagonale, et rien qu'elle. L'ancienne cle maitresse n'ouvre plus aucune des six. UN SEUL MOT DE PASSE NE POUVAIT PLUS SUFFIRE, et pas pour la raison qu'on croit : `cloner_vm_debian.yml` charge la voute du TENANT puis celle de l'UNDERLAY dans la meme execution. `ANSIBLE_VAULT_IDENTITY_LIST` en porte plusieurs et les essaie toutes — un seul export suffit pour les 28 appels a ansible-playbook, sans en toucher un seul. La liste se derive dans scripts/voutes.py. CE QUI BORNE LE POUVOIR N'EST PAS LA LISTE MAIS LA PRESENCE DES FICHIERS. Sur le poste, toutes les cles sont la — c'est l'humain qui les detient toutes, et P03 lit les inventaires de tous les freres. Sur un runner, une seule existe. Le code est identique, le pouvoir ne l'est pas. DEUX PREUVES ONT DIT CE QUI MANQUAIT. P03 est tombee des la separation : elle lit les inventaires voisins, donc il lui faut leurs cles — c'est elle qui a etabli que la liste devait couvrir le voisinage. P16 s'est mise a SAUTER : sa garde ne connaissait que ANSIBLE_VAULT_PASSWORD_FILE. Une preuve sautee se lit trop facilement comme une preuve passee. COUT ASSUME : le chiffre et sa cle cohabiteront sur la meme machine des que les runners recevront la leur. Le pari tient parce que le perimetre est borne. Les six voutes sauvegardees avant rechiffrement. L'ancienne cle maitresse reste sur le poste : la retirer est une decision, pas un nettoyage. make verifier : vert. make prouver : CONFORME, 52 OK, 0 echec, 0 saute — sans ANSIBLE_VAULT_PASSWORD_FILE. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-08-28 14:23:33 -04:00
if [[ -n "$$vault_chiffre" && -z "$${ANSIBLE_VAULT_PASSWORD_FILE:-}$${ANSIBLE_VAULT_IDENTITY_LIST:-}" ]]; then \
if [[ -t 0 ]]; then \
read -r -s -p 'Mot de passe du vault Ansible (demande une seule fois): ' mdp; echo; \
vf="$$(mktemp)"; printf '%s' "$$mdp" > "$$vf"; chmod 600 "$$vf"; \
export ANSIBLE_VAULT_PASSWORD_FILE="$$vf"; \
trap 'rm -f "$$vf"' EXIT; \
else \
printf '%s\n' 'Refus: vault chiffre detecte mais aucun mot de passe (entree non interactive). Fournir ANSIBLE_VAULT_PASSWORD_FILE ou le champ vault de la GUI.'; \
exit 2; \
fi; \
fi;
endef
perf : forks=20 et creation des VM en parallele Choisis sur le chronometrage d'une reconstruction complete (1 h 12 min 48 s, phase par phase), pas sur l'intuition. FORKS ETAIT AU DEFAUT (5) POUR 14 HOTES. Chaque play qui balaie la flotte — socle, durcissement, enrolement PKI, les cinq agents — tournait en TROIS vagues. Les gros roles n'en profitent pas : Nextcloud (10 min 55 s), Forgejo, Grafana et Keycloak sont sur UN hote. Ce sont les couches larges qui payaient. forks = 20 laisse de la marge au-dela des 14 actuels ; chaque fork est un processus sur le controleur, pas sur les cibles. LA CREATION DES VM SE FAISAIT UNE PAR UNE : 14 x 1 min 35 s = 22 min 08 s, 30 % du total, et surtout de l'ATTENTE (clone, demarrage, SSH, verrou dpkg) sans le moindre recouvrement. flotte-creer en lance quatre a la fois, ajustable par PARALLELE=n. La sortie de chaque hote va dans SON fichier, recopiee ordonnee a la fin : quatre clones ecrivant en meme temps donneraient un journal illisible, ce qui serait un comble apres une journee a traquer des diagnostics masques. Le marqueur `=== Creation VM: <hote> ===` reste emis en direct. Et un echec n'est pas avale : le code de retour de chaque hote est relu, et la cible sort en erreur si l'un a echoue — sinon _attendre-flotte partirait sur une flotte incomplete. Gains ESTIMES, a verifier par la prochaine mesure : ~15 min sur les clones, ~5 min sur les couches larges. Le troisieme levier identifie — l'archive Nextcloud en .tar.bz2, decompressee sur un seul coeur — attend d'etre mesure. Verifie : ansible-config confirme forks=20, make -n passe, prouver.py 35 OK. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 20:40:25 -04:00
flotte-creer: _instance-requise ## Cree les VM manquantes de la flotte depuis le plan — PARALLELE=n
@set -e; \
if [[ "$(CONFIRMER)" != "true" ]]; then \
printf '%s\n' 'Refus: creation de TOUTES les VM actives du plan (clone Proxmox).'; \
printf '%s\n' 'Relancer avec CONFIRMER=true.'; \
exit 2; \
fi; \
hotes="$$(python3 scripts/inventory_host.py --inventaire $(INVENTAIRE_PRODUCTION) lister-actifs)"; \
if [[ -z "$$hotes" ]]; then printf '%s\n' 'Refus: aucun hote actif dans le plan.'; exit 2; fi; \
perf : forks=20 et creation des VM en parallele Choisis sur le chronometrage d'une reconstruction complete (1 h 12 min 48 s, phase par phase), pas sur l'intuition. FORKS ETAIT AU DEFAUT (5) POUR 14 HOTES. Chaque play qui balaie la flotte — socle, durcissement, enrolement PKI, les cinq agents — tournait en TROIS vagues. Les gros roles n'en profitent pas : Nextcloud (10 min 55 s), Forgejo, Grafana et Keycloak sont sur UN hote. Ce sont les couches larges qui payaient. forks = 20 laisse de la marge au-dela des 14 actuels ; chaque fork est un processus sur le controleur, pas sur les cibles. LA CREATION DES VM SE FAISAIT UNE PAR UNE : 14 x 1 min 35 s = 22 min 08 s, 30 % du total, et surtout de l'ATTENTE (clone, demarrage, SSH, verrou dpkg) sans le moindre recouvrement. flotte-creer en lance quatre a la fois, ajustable par PARALLELE=n. La sortie de chaque hote va dans SON fichier, recopiee ordonnee a la fin : quatre clones ecrivant en meme temps donneraient un journal illisible, ce qui serait un comble apres une journee a traquer des diagnostics masques. Le marqueur `=== Creation VM: <hote> ===` reste emis en direct. Et un echec n'est pas avale : le code de retour de chaque hote est relu, et la cible sort en erreur si l'un a echoue — sinon _attendre-flotte partirait sur une flotte incomplete. Gains ESTIMES, a verifier par la prochaine mesure : ~15 min sur les clones, ~5 min sur les couches larges. Le troisieme levier identifie — l'archive Nextcloud en .tar.bz2, decompressee sur un seul coeur — attend d'etre mesure. Verifie : ansible-config confirme forks=20, make -n passe, prouver.py 35 OK. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 20:40:25 -04:00
par="$(PARALLELE)"; par="$${par:-4}"; \
tmp="$$(mktemp -d)"; trap 'rm -rf "$$tmp"' EXIT; \
$(VAULT_UNE_FOIS) \
perf : forks=20 et creation des VM en parallele Choisis sur le chronometrage d'une reconstruction complete (1 h 12 min 48 s, phase par phase), pas sur l'intuition. FORKS ETAIT AU DEFAUT (5) POUR 14 HOTES. Chaque play qui balaie la flotte — socle, durcissement, enrolement PKI, les cinq agents — tournait en TROIS vagues. Les gros roles n'en profitent pas : Nextcloud (10 min 55 s), Forgejo, Grafana et Keycloak sont sur UN hote. Ce sont les couches larges qui payaient. forks = 20 laisse de la marge au-dela des 14 actuels ; chaque fork est un processus sur le controleur, pas sur les cibles. LA CREATION DES VM SE FAISAIT UNE PAR UNE : 14 x 1 min 35 s = 22 min 08 s, 30 % du total, et surtout de l'ATTENTE (clone, demarrage, SSH, verrou dpkg) sans le moindre recouvrement. flotte-creer en lance quatre a la fois, ajustable par PARALLELE=n. La sortie de chaque hote va dans SON fichier, recopiee ordonnee a la fin : quatre clones ecrivant en meme temps donneraient un journal illisible, ce qui serait un comble apres une journee a traquer des diagnostics masques. Le marqueur `=== Creation VM: <hote> ===` reste emis en direct. Et un echec n'est pas avale : le code de retour de chaque hote est relu, et la cible sort en erreur si l'un a echoue — sinon _attendre-flotte partirait sur une flotte incomplete. Gains ESTIMES, a verifier par la prochaine mesure : ~15 min sur les clones, ~5 min sur les couches larges. Le troisieme levier identifie — l'archive Nextcloud en .tar.bz2, decompressee sur un seul coeur — attend d'etre mesure. Verifie : ansible-config confirme forks=20, make -n passe, prouver.py 35 OK. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 20:40:25 -04:00
printf 'Creation de %s VM, %s a la fois.\n' "$$(echo $$hotes | wc -w)" "$$par"; \
n=0; \
for h in $$hotes; do \
printf '\n=== Creation VM: %s ===\n' "$$h"; \
perf : forks=20 et creation des VM en parallele Choisis sur le chronometrage d'une reconstruction complete (1 h 12 min 48 s, phase par phase), pas sur l'intuition. FORKS ETAIT AU DEFAUT (5) POUR 14 HOTES. Chaque play qui balaie la flotte — socle, durcissement, enrolement PKI, les cinq agents — tournait en TROIS vagues. Les gros roles n'en profitent pas : Nextcloud (10 min 55 s), Forgejo, Grafana et Keycloak sont sur UN hote. Ce sont les couches larges qui payaient. forks = 20 laisse de la marge au-dela des 14 actuels ; chaque fork est un processus sur le controleur, pas sur les cibles. LA CREATION DES VM SE FAISAIT UNE PAR UNE : 14 x 1 min 35 s = 22 min 08 s, 30 % du total, et surtout de l'ATTENTE (clone, demarrage, SSH, verrou dpkg) sans le moindre recouvrement. flotte-creer en lance quatre a la fois, ajustable par PARALLELE=n. La sortie de chaque hote va dans SON fichier, recopiee ordonnee a la fin : quatre clones ecrivant en meme temps donneraient un journal illisible, ce qui serait un comble apres une journee a traquer des diagnostics masques. Le marqueur `=== Creation VM: <hote> ===` reste emis en direct. Et un echec n'est pas avale : le code de retour de chaque hote est relu, et la cible sort en erreur si l'un a echoue — sinon _attendre-flotte partirait sur une flotte incomplete. Gains ESTIMES, a verifier par la prochaine mesure : ~15 min sur les clones, ~5 min sur les couches larges. Le troisieme levier identifie — l'archive Nextcloud en .tar.bz2, decompressee sur un seul coeur — attend d'etre mesure. Verifie : ansible-config confirme forks=20, make -n passe, prouver.py 35 OK. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 20:40:25 -04:00
( $(MAKE) creer-vm HOTE="$$h" > "$$tmp/$$h.log" 2>&1; printf '%s' "$$?" > "$$tmp/$$h.rc" ) & \
n=$$((n+1)); \
if (( n >= par )); then wait -n || true; n=$$((n-1)); fi; \
done; \
perf : forks=20 et creation des VM en parallele Choisis sur le chronometrage d'une reconstruction complete (1 h 12 min 48 s, phase par phase), pas sur l'intuition. FORKS ETAIT AU DEFAUT (5) POUR 14 HOTES. Chaque play qui balaie la flotte — socle, durcissement, enrolement PKI, les cinq agents — tournait en TROIS vagues. Les gros roles n'en profitent pas : Nextcloud (10 min 55 s), Forgejo, Grafana et Keycloak sont sur UN hote. Ce sont les couches larges qui payaient. forks = 20 laisse de la marge au-dela des 14 actuels ; chaque fork est un processus sur le controleur, pas sur les cibles. LA CREATION DES VM SE FAISAIT UNE PAR UNE : 14 x 1 min 35 s = 22 min 08 s, 30 % du total, et surtout de l'ATTENTE (clone, demarrage, SSH, verrou dpkg) sans le moindre recouvrement. flotte-creer en lance quatre a la fois, ajustable par PARALLELE=n. La sortie de chaque hote va dans SON fichier, recopiee ordonnee a la fin : quatre clones ecrivant en meme temps donneraient un journal illisible, ce qui serait un comble apres une journee a traquer des diagnostics masques. Le marqueur `=== Creation VM: <hote> ===` reste emis en direct. Et un echec n'est pas avale : le code de retour de chaque hote est relu, et la cible sort en erreur si l'un a echoue — sinon _attendre-flotte partirait sur une flotte incomplete. Gains ESTIMES, a verifier par la prochaine mesure : ~15 min sur les clones, ~5 min sur les couches larges. Le troisieme levier identifie — l'archive Nextcloud en .tar.bz2, decompressee sur un seul coeur — attend d'etre mesure. Verifie : ansible-config confirme forks=20, make -n passe, prouver.py 35 OK. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 20:40:25 -04:00
wait; \
echec=0; \
for h in $$hotes; do \
printf '\n----- journal de %s -----\n' "$$h"; \
cat "$$tmp/$$h.log" 2>/dev/null || printf '(aucune sortie)\n'; \
rc="$$(cat "$$tmp/$$h.rc" 2>/dev/null || printf 1)"; \
if [[ "$$rc" != 0 ]]; then printf '!! %s a ECHOUE (code %s)\n' "$$h" "$$rc"; echec=1; fi; \
done; \
if (( echec )); then printf '\nAu moins une VM n a pas ete creee — rien ne continue.\n'; exit 2; fi; \
printf '\nToutes les VM actives sont creees.\n'
# --- LES MACHINES DU SITE -----------------------------------------------------
#
# UN SITE N'EST PAS UN PLAN. Un tenant se derive de son `index` ; un site n'en a pas et
# ne derive de rien — il EST le terrain sur lequel les tenants derivent. Ses machines se
# declarent dans `underlay.yml`, a cote des switches et des hyperviseurs qui les portent.
#
# D'ou des cibles distinctes de `flotte-*` : elles ne demandent AUCUNE instance montee.
# Le site existe avant tout tenant, et doit pouvoir naitre quand aucun n'est encore la.
#
# Le clone lui-meme reste `cloner-vm` — le seul chemin eprouve, et il n'y en aura pas
# deux. `site_machines.py` ne fait que traduire la declaration en ses parametres.
.PHONY: site-decrire
site-decrire: ## Les machines de l'hebergeur declarees dans l'underlay
@python3 scripts/site_machines.py decrire
.PHONY: site-inventaire
site-inventaire: ## L'inventaire dynamique du site, tel qu'Ansible le voit
@ansible-inventory -i scripts/site_inventaire.py --graph
.PHONY: site-appliquer
site-appliquer: ## Applique un role aux machines du site — GROUPE=<serveur_ops_site|...>
@# `GROUPE` a un defaut global (GROUPE_DEBIAN) : tester s'il est VIDE ne protege de
@# rien, ce garde-fou-la ne pourrait jamais se declencher. Le vrai risque, observe :
@# lancer un playbook de TENANT contre l'inventaire du SITE. Ansible n'y trouve aucun
@# hote, n'a rien a faire, et sort avec 0 — un succes qui n'a rien fait. On refuse donc
@# tout groupe qui n'existe pas DANS CET INVENTAIRE-CI.
site : un ecosysteme complet — PKI, DNS, forge, cache, runner Le site portait trois services sur les sept du modele `origine`. Sa forge servait LE GENOME EN CLAIR et aucune de ses machines n'avait de certificat — donc aucun chiffrement est-ouest, ce que la doctrine zero-confiance interdit. site-pki-01 (step-ca) et site-dns-01 (PowerDNS + resolveur colocalises) rejoignent les trois autres. Quatre defauts que le site a fait tomber, chacun invisible chez un tenant : - DNS bloque par notre propre default-deny. Un tenant a son resolveur DANS son reseau et ne traverse jamais la frontiere ; le site interroge la sienne. La regle est derivee de `site.dns_amorcage`, destination declaree, jamais `any`. - serveur_cache_site n'installe rien : il marque un cache et lit les variables de serveur_artefacts. Les defauts d'un role ne sont en portee que dans le play qui l'inclut — une dependance de role regle l'ordre ET la portee. - resoudre_idp partait meme avec OIDC desactive, et exigeait un plan. La resolution suit desormais l'usage. - le plancher /etc/hosts etait VIDE : `hotes_actifs` n'existait pas dans l'inventaire du site. Un role qui reussit en n'ecrivant rien est la pire forme d'echec. Une machine du site peut desormais se configurer (`variables:`), appliquee en dernier : ce qu'une machine declare d'elle-meme prime sur ce que le site declare pour toutes. Verifie et non suppose : systemd disait `active` mais rien n'ecoutait sur 443 — step-ca sert sur 8443. La zone souveraine resout et la recursion marche, mesurees sur la machine. 42 preuves vertes, ansible-lint profil production. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 12:00:17 -04:00
@#
@# LA VOUTE DU SITE est derivee du symlink `underlay.yml`, jamais redeclaree. Les roles
@# partages exigent leurs secrets (Forgejo : secret_key, internal_token, admin_password).
@# Un tenant les tient de `group_vars/all/vault.yml` ; le site les tient de
@# `underlay.vault.yml`, qui vit a cote de sa carte et hors depot. Sans le `-e @`, le
@# role echoue sur son assertion — et l'echec ne dit pas qu'il manque un FICHIER,
@# seulement que les valeurs sont vides.
@set -e; \
if [[ ! -f "playbooks/groupes/$(GROUPE).yml" ]]; then \
printf 'Refus: aucun playbook pour le groupe %s.\n' "$(GROUPE)"; exit 2; \
fi; \
connus="$$(python3 scripts/site_inventaire.py --list | python3 -c \
'import json,sys; print(" ".join(k for k in json.load(sys.stdin) if k != "_meta"))')"; \
if [[ " $$connus " != *" $(GROUPE) "* ]]; then \
printf 'Refus: le groupe %s n existe pas au site.\n' "$(GROUPE)"; \
printf 'Groupes du site : %s\n' "$$connus"; \
exit 2; \
fi; \
voute="$$(dirname "$$(readlink -f underlay.yml)")/underlay.vault.yml"; \
supp=(); [[ -f "$$voute" ]] && supp+=( -e "@$$voute" ); \
ansible-playbook -i scripts/site_inventaire.py "playbooks/groupes/$(GROUPE).yml" \
"$${supp[@]}" $(ARGS)
site : le runner travaille, et le genome remonte chez lui `serveur_ops` et `serveur_ops_site` sont deployes sur `site-ops-01` : le moteur, les plans des trois tenants, les collections hors ligne, et la voute de l'underlay deposee CHIFFREE. Le site a son runner. DEUX FORMES DE RUNNER, ET UNE SEULE ETAIT PREVUE. `serveur_ops` exigeait un depot `role: instance` et un `serveur_ops_instance` — la forme d'un runner de TENANT, qui pilote un ecosysteme, monte son plan par le lien `instance`, detient sa voute. Un runner de SITE ne pilote rien : il MATERIALISE le terrain que d'autres occuperont, et son inventaire est dynamique. Son plan declarait donc ses depots de tenants en `role: tenant`, a dessein et avec ses raisons ecrites — et le role le refusait au nom d'une exigence qui ne le concernait pas. Il exige desormais que le runner pilote QUELQUE CHOSE, sans prescrire quoi. Et il posait le lien quand meme : `serveur_ops_instance: ""` donnait `instance -> /opt/setops/`, un repertoire qui n'est l'ecosysteme de personne. Un lien qui existe et ne designe rien est pire qu'un lien absent : tout ce qui le suit croit avoir trouve une instance. LE CERTIFICAT COUVRE LES NOMS DU SERVICE. `client_pki` ne mettait dans ses SAN que le FQDN, le nom court et l'IP. Le clonage du genome a bute dessus : « certificate subject name (site-forge-01.genese.internal) does not match target hostname 'forge.genese.internal' ». Le nom declare `expose:` etait publie partout — plancher, zone DNS — et couvert nulle part. Les expositions que cet hote SERT REELLEMENT entrent maintenant dans le certificat. Un certificat qui revendiquerait le nom d'un service rendu ailleurs serait une usurpation, pas une commodite. `make genome-pousser` — LE RUNNER POUSSE, LE POSTE NE ROUTE PAS. La forge du genome vit DANS le site, sur un reseau que le poste de l'exploitant ne route pas. On ne perce pas un chemin pour lui : on lui retire le role. Le poste emballe les commits dans un `git bundle` — un fichier, verifiable, qui ne demande aucun reseau — et le runner verifie, avance EN AVANCE RAPIDE SEULEMENT, puis pousse avec SA cle, autorisee en ecriture sur ces depots-la seulement. Ce qui est pousse se DERIVE de `serveur_ops_depots`. `DEPOT=<nom>` en cible un seul. Trois pieges rencontres, tous inscrits dans le code : l'outil ne peut pas etre supposé present sur le runner (il ne recevrait cette version qu'APRES la poussee qu'elle sert a faire — le controleur le porte) ; la branche n'est pas toujours `main` (deux depots vivent sur `master`, et le plan le declarait deja) ; le dossier des depots freres contient un espace, d'ou `argv` et non `cmd`. Le juge n'est pas le journal du playbook mais LA FORGE : on demande a son API ce qu'elle porte, et on refuse si ca differe de ce que porte le runner. Six depots verifies. Pourquoi ca comptait : la forge du site etait restee quatre commits derriere le poste, sans que rien ne le signale — dont le correctif qui desarme le pare-feu Proxmox. Un ecosysteme qui se reproduit depuis une forge en retard reproduit ses defauts. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 08:52:37 -04:00
# --- LE GENOME REMONTE SUR LA FORGE DU SITE -----------------------------------
#
# La forge du genome vit DANS le site, sur un reseau que le poste de l'exploitant ne route
# pas — et c'est voulu : le poste n'a de patte que sur l'administration, la frontiere
# police le reste. Il ne peut donc pas pousser lui-meme.
#
# On ne perce pas un chemin pour lui. Le RUNNER est dans le site, il porte le genome, il a
# une identite : on lui PORTE les commits dans un `git bundle` — un fichier, verifiable,
# qui ne demande aucun reseau — et c'est LUI qui pousse, avec SA cle.
#
# Mesure du 2026-08-26 : la forge du site etait restee quatre commits derriere le poste,
# sans que rien ne le signale. Le runner clonait donc un moteur perime — dont, ce jour-la,
# le correctif qui desarme le pare-feu Proxmox. Un ecosysteme qui se reproduit depuis une
# forge en retard reproduit ses defauts.
#
# CE QUI EST POUSSE SE DERIVE de `serveur_ops_depots` (au plan du site) : on pousse ceux
# dont une copie locale existe a cote du moteur, et rien d'autre.
gabarit : q35 n est pas un reglage, c est la raison de la procedure manuelle CONSTAT DE L EXPLOITANT, PAYE EN ANOMALIES : convertir une machine deja installee d `i440fx` a `q35` produit une serie de pannes dont chacune ressemble a autre chose qu a sa cause. Ce n est pas une correction, c est une transplantation. LE MECANISME, ECRIT POUR QU ON NE LE REDECOUVRE PAS : `i440fx` est un chipset PCI, `q35` est PCIe. La topologie des bus change, donc les NOMS D INTERFACES PREDICTIBLES changent avec le chemin PCI (enp0s3 -> enp1s0) et la machine perd le reseau ; les chemins de disques bougent ; l ordre d enumeration suit. C EST AUSSI POURQUOI SET-OPS N UTILISE PAS L IMAGE CLOUD OFFICIELLE DE DEBIAN : `genericcloud` est livree configuree pour `i440fx`. Une machine nait `q35`, ou elle ne le sera jamais proprement — et c est ce que l installation depuis l ISO garantit. Ces deux lignes de la procedure n etaient qu une ligne de tableau. Elles portent maintenant leur pourquoi, et le SITE les declare comme DONNEES (cle `gabarit`), plus seulement comme prose. `make gabarit-etat` compare le gabarit reel a ce que le site declare de lui. ON VERIFIE LA SOURCE, PAS CHAQUE COPIE. Ma premiere version gardait le CLONAGE : la propriete s herite, donc verifier chaque clone coute a chaque creation sans rien dire de plus que verifier le gabarit une fois. Retiree. TROIS FOIS J AI DEVINE LA FORME DE LA REPONSE AU LIEU DE LA REGARDER — regex_search a groupe qui rend None, proxmox_vm_info sans `config: current` qui ne rend que l etat. La garde a declare « ? » sur une VM parfaitement conforme : une garde qui crie toujours est pire qu aucune, on apprend a l ignorer. A la demande et non dans `make prouver` : ce controle exige le cluster, que le harnais ne suppose pas joignable. Meme nature que genome-etat et underlay-plan. Controle negatif verifie : declarer i440fx fait echouer, rc=1. make verifier : vert. make prouver : CONFORME, 56 OK, 0 echec. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-09-01 14:42:58 -04:00
# LE GABARIT PORTE-T-IL CE QUE LE SITE DECLARE DE LUI ?
#
# Son type de machine ne se corrige pas apres coup : convertir `i440fx` -> `q35` remplace
# le materiel sous un systeme qui croit connaitre le sien, et chaque panne qui s'ensuit
# ressemble a autre chose qu'a sa cause. On verifie donc la SOURCE — la propriete s'herite,
# tous les clones d'un gabarit conforme le sont.
#
# A LA DEMANDE, pas dans `make prouver` : ce controle exige le cluster, que le harnais ne
# suppose pas joignable. Meme nature que `genome-etat` et `underlay-plan`.
le site surveille enfin sa fabric La supervision du site voyait ses sept VM et rien d autre. Au depart, depuis site-mon-01, les trois hyperviseurs, les neuf pattes de la frontiere et sa PROPRE passerelle par defaut rendaient tous 100 pourcent de perte au ping. 16 hotes UP sur 16 dont 9 pattes de frontiere en controle ACTIF 11 cibles Prometheus dont 3 hyperviseurs, job fabric separe LE PARTAGE. Les hyperviseurs portent node_exporter - D-48 l autorise - et exposent 1005 unites systemd avec leur etat, soit ce que la sonde sante mesurait, plus la charge et le disque. Ils n entrent PAS dans le socle : leur appliquer serveur_durci reecrirait le pare-feu, le SSH et les sysctl de la machine qui tient tout le reste. La frontiere, elle, n accueille aucun agent : controle actif, une entree par PATTE, parce qu une interface eteinte coupe une zone pendant que les autres vont bien. ON TIRE, ON NE POUSSE PAS. J avais propose du passif et il avait ete valide ; la mesure a dit non. Un hyperviseur envoie vers un routeur qui ne connait pas les reseaux du site, et sa route par defaut est GELEE (D-57). Le porteur de sante y expirait en 20 s. LE VRAI DEFAUT ETAIT UNE LISTE QUI N A PAS SUIVI. Le mecanisme de routage existait deja sur vmbr0, avec un commentaire du 2026-08-26 tenant exactement le raisonnement qu on venait de refaire. Sa liste s arretait a 10.0.34.0/24 quand le site en declare six : les zones sauvegarde et supervision sont nees, les routes n ont pas suivi. Le symptome ne ressemblait pas a une route manquante - il ressemblait a un pare-feu, puis a un probleme de reseau chez l exploitant. make routes-fabric-etat compare desormais TROIS choses : zones declarees, routes declarees dans /etc/network/interfaces, routes vivantes dans le noyau. Le cas le plus traitre est vivante mais non declaree : tout fonctionne, la supervision est verte, et la panne attend la prochaine maintenance. Eprouvee dans les deux sens. Deux defauts trouves en construisant. bifrost-2 est un nom RESERVE, pas un boitier - l underlay le disait en prose, illisible par le moteur ; il porte desormais etat: reserve. Et un service passif n existe que pour un hote qui peut POUSSER : l appartenance a un groupe sert deux choses qui ne coincident pas toujours, a qui l on deploie et de qui l on attend un rapport. Les commutateurs restent dehors, choix de l exploitant, coherent avec D-48. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-10 18:50:43 -04:00
.PHONY: routes-fabric-etat
routes-fabric-etat: ansible-runtime ## Les hyperviseurs routent-ils toutes les zones du SITE ? — ne corrige rien
@# UNE LISTE QUI DOIT SUIVRE UNE AUTRE LISTE PREND DU RETARD. Les routes specifiques
@# de `vmbr0` s'arretaient a 10.0.34.0/24 quand le site en declarait six : les zones
@# `sauvegarde` et `supervision` etaient nees sans que les routes suivent. Le symptome
@# ne ressemblait pas a une route manquante — il ressemblait a un pare-feu.
python3 scripts/routes_fabric_etat.py
.PHONY: expositions-etat
expositions-etat: ## Chaque exposition est-elle servie sous un certificat qui la porte ? — ne corrige rien
@# `serveur_nginx` POSE LE VHOST ET LAISSE LE SAN EN ARRIERE. Mesure deux fois le
@# 2026-09-10, sur `grafana -> observatoire` puis `icinga -> vigie` : la zone publiait
@# le nouveau nom, le vhost repondait, le client OIDC l'acceptait — et le certificat
@# servi portait encore l'ancien. C'est `client_pki` qui reemet, et rien ne le reclame.
@# Ajouter `--site` pour l'ecosysteme du SITE.
python3 scripts/expositions_etat.py $(if $(SITE),--site)
gabarit : q35 n est pas un reglage, c est la raison de la procedure manuelle CONSTAT DE L EXPLOITANT, PAYE EN ANOMALIES : convertir une machine deja installee d `i440fx` a `q35` produit une serie de pannes dont chacune ressemble a autre chose qu a sa cause. Ce n est pas une correction, c est une transplantation. LE MECANISME, ECRIT POUR QU ON NE LE REDECOUVRE PAS : `i440fx` est un chipset PCI, `q35` est PCIe. La topologie des bus change, donc les NOMS D INTERFACES PREDICTIBLES changent avec le chemin PCI (enp0s3 -> enp1s0) et la machine perd le reseau ; les chemins de disques bougent ; l ordre d enumeration suit. C EST AUSSI POURQUOI SET-OPS N UTILISE PAS L IMAGE CLOUD OFFICIELLE DE DEBIAN : `genericcloud` est livree configuree pour `i440fx`. Une machine nait `q35`, ou elle ne le sera jamais proprement — et c est ce que l installation depuis l ISO garantit. Ces deux lignes de la procedure n etaient qu une ligne de tableau. Elles portent maintenant leur pourquoi, et le SITE les declare comme DONNEES (cle `gabarit`), plus seulement comme prose. `make gabarit-etat` compare le gabarit reel a ce que le site declare de lui. ON VERIFIE LA SOURCE, PAS CHAQUE COPIE. Ma premiere version gardait le CLONAGE : la propriete s herite, donc verifier chaque clone coute a chaque creation sans rien dire de plus que verifier le gabarit une fois. Retiree. TROIS FOIS J AI DEVINE LA FORME DE LA REPONSE AU LIEU DE LA REGARDER — regex_search a groupe qui rend None, proxmox_vm_info sans `config: current` qui ne rend que l etat. La garde a declare « ? » sur une VM parfaitement conforme : une garde qui crie toujours est pire qu aucune, on apprend a l ignorer. A la demande et non dans `make prouver` : ce controle exige le cluster, que le harnais ne suppose pas joignable. Meme nature que genome-etat et underlay-plan. Controle negatif verifie : declarer i440fx fait echouer, rc=1. make verifier : vert. make prouver : CONFORME, 56 OK, 0 echec. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-09-01 14:42:58 -04:00
.PHONY: gabarit-etat
gabarit-etat: ## Le gabarit porte-t-il ce que le SITE declare ? — ne corrige rien, regarde
python3 scripts/gabarit_etat.py
.PHONY: genome-etat
genome-etat: ## Ce que la forge du site porte, face au poste — ne corrige rien, regarde
@set -e; \
voute="$$(dirname "$$(readlink -f underlay.yml)")/underlay.vault.yml"; \
supp=(); [[ -f "$$voute" ]] && supp+=( -e "@$$voute" ); \
ansible-playbook -i scripts/site_inventaire.py \
playbooks/maintenance/genome_etat.yml "$${supp[@]}" $(ARGS)
site : le runner travaille, et le genome remonte chez lui `serveur_ops` et `serveur_ops_site` sont deployes sur `site-ops-01` : le moteur, les plans des trois tenants, les collections hors ligne, et la voute de l'underlay deposee CHIFFREE. Le site a son runner. DEUX FORMES DE RUNNER, ET UNE SEULE ETAIT PREVUE. `serveur_ops` exigeait un depot `role: instance` et un `serveur_ops_instance` — la forme d'un runner de TENANT, qui pilote un ecosysteme, monte son plan par le lien `instance`, detient sa voute. Un runner de SITE ne pilote rien : il MATERIALISE le terrain que d'autres occuperont, et son inventaire est dynamique. Son plan declarait donc ses depots de tenants en `role: tenant`, a dessein et avec ses raisons ecrites — et le role le refusait au nom d'une exigence qui ne le concernait pas. Il exige desormais que le runner pilote QUELQUE CHOSE, sans prescrire quoi. Et il posait le lien quand meme : `serveur_ops_instance: ""` donnait `instance -> /opt/setops/`, un repertoire qui n'est l'ecosysteme de personne. Un lien qui existe et ne designe rien est pire qu'un lien absent : tout ce qui le suit croit avoir trouve une instance. LE CERTIFICAT COUVRE LES NOMS DU SERVICE. `client_pki` ne mettait dans ses SAN que le FQDN, le nom court et l'IP. Le clonage du genome a bute dessus : « certificate subject name (site-forge-01.genese.internal) does not match target hostname 'forge.genese.internal' ». Le nom declare `expose:` etait publie partout — plancher, zone DNS — et couvert nulle part. Les expositions que cet hote SERT REELLEMENT entrent maintenant dans le certificat. Un certificat qui revendiquerait le nom d'un service rendu ailleurs serait une usurpation, pas une commodite. `make genome-pousser` — LE RUNNER POUSSE, LE POSTE NE ROUTE PAS. La forge du genome vit DANS le site, sur un reseau que le poste de l'exploitant ne route pas. On ne perce pas un chemin pour lui : on lui retire le role. Le poste emballe les commits dans un `git bundle` — un fichier, verifiable, qui ne demande aucun reseau — et le runner verifie, avance EN AVANCE RAPIDE SEULEMENT, puis pousse avec SA cle, autorisee en ecriture sur ces depots-la seulement. Ce qui est pousse se DERIVE de `serveur_ops_depots`. `DEPOT=<nom>` en cible un seul. Trois pieges rencontres, tous inscrits dans le code : l'outil ne peut pas etre supposé present sur le runner (il ne recevrait cette version qu'APRES la poussee qu'elle sert a faire — le controleur le porte) ; la branche n'est pas toujours `main` (deux depots vivent sur `master`, et le plan le declarait deja) ; le dossier des depots freres contient un espace, d'ou `argv` et non `cmd`. Le juge n'est pas le journal du playbook mais LA FORGE : on demande a son API ce qu'elle porte, et on refuse si ca differe de ce que porte le runner. Six depots verifies. Pourquoi ca comptait : la forge du site etait restee quatre commits derriere le poste, sans que rien ne le signale — dont le correctif qui desarme le pare-feu Proxmox. Un ecosysteme qui se reproduit depuis une forge en retard reproduit ses defauts. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 08:52:37 -04:00
.PHONY: genome-pousser
genome-pousser: ## Le runner du site pousse le genome sur la forge du site — DEPOT=<nom> pour un seul
@set -e; \
voute="$$(dirname "$$(readlink -f underlay.yml)")/underlay.vault.yml"; \
supp=(); [[ -f "$$voute" ]] && supp+=( -e "@$$voute" ); \
SETOPS_DEPOT="$(DEPOT)" ansible-playbook -i scripts/site_inventaire.py \
playbooks/maintenance/genome_pousser.yml "$${supp[@]}" $(ARGS)
.PHONY: site-creer
le site reconstruit depuis zero : 7/7, 0 echec, et seize corrections SITE-Chezlepro n avait jamais ete rase. La limite qu on repetait partout — l infrastructure d accueil n a jamais ete reconstruite depuis zero — se lisait comme de la prudence. C etait seize defauts que rien d autre n aurait pu reveler. Un locataire naît dans un monde deja peuple : le site lui fournit paquets, noms, genome, heure et depot. Un site n a personne au-dessus, sauf sa frontiere. Onze des seize murs viennent de la. DEUX CAPACITES QUI N EXISTAIENT PAS. make site-raser — rien ne detruisait les machines du site, donc la limite etait un trou d outillage. make forge-amorcer — la forge naît vide et le runner y clone ; l amorcage part du poste, seul endroit qui detienne alors le genome. TROIS GARDES QUI VERIFIAIENT LA FORME. La plus couteuse des familles : elles donnent l apparence d une verification. Le resolveur comparait des adresses au lieu de mesurer si la resolution aboutit, et protegeait ainsi l etat casse. L administration etait reconnue a son port. Un flux a deux paires n obtenait qu une branche. UN ECART DE SECURITE. Le PostgreSQL du site servait le certificat auto-signe de Debian, sans reseaux autorises ni hostssl — invisible tant qu aucun client n exigeait la verification. Le defaut n a pas casse la construction : la construction a revele le defaut. UN ACCES ACCIDENTEL. Celui de l exploitant tenait au chevauchement d adressage que le renumerotage a supprime. Separer les index n a pas cause le probleme, il a retire le hasard qui le masquait. La sequence du premier jour est ecrite : runbooks §9, avec les seize murs et ce que chacun enseigne. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-12 19:08:31 -04:00
forge-amorcer: ## Amorce la forge d'un SITE neuf avec le genome — CONFIRMER=true
@# LE MAILLON QUI N'EXISTAIT PAS (2026-09-12, premiere reconstruction du site depuis
@# zero). Le runner clone le genome depuis SA PROPRE forge, et `serveur_forgejo` ne
@# cree ni organisation ni depot : a froid la forge naît vide et le clone echoue.
@#
@# Le deploiement n'avait jamais rencontre ce cas — la forge existait depuis le premier
@# jour, remplie a la main, a une date que personne n'a notee.
@#
@# DEPUIS LE POSTE, et c'est structurel : a cet instant il est le SEUL endroit qui
@# detienne le genome. Un amorcage vient toujours de l'exterieur de ce qu'il amorce.
@#
@# export SETOPS_FORGE_MDP=… puis make forge-amorcer CONFIRMER=true
python3 scripts/forge_amorcer.py $(if $(filter true,$(CONFIRMER)),--confirmer) \
$(if $(FORGE),--forge "$(FORGE)")
.PHONY: forge-amorcer
le site prend l index 37, et la capacite de le raser existe enfin SITE-Chezlepro passe de 10.0.31-36 (tapes a la main, derives de rien) et 10.17.0.0/24 (emprunte au supernet du locataire) a son PROPRE index : gestion en 10.37.0.0/24, zones en 10.37.31-36. OPS-Chezlepro garde 17. CE QUE L OPERATION A REVELE. Il n existait aucun moyen de raser le site : raser.py ne vise que l instance active, un locataire. Ce n est donc pas que personne n avait essaye de le reconstruire depuis zero — l outil n en offrait pas le moyen, et la limite se lisait partout sans que sa cause soit nommee. make site-raser reprend les quatre verrous de raser.py. TROIS FOIS LE MEME DEFAUT, attrape par l exploitant. La declaration decrit la CIBLE pendant que l outillage s en sert pour joindre l EXISTANT : renumeroter le rebond avant de bouger la patte a rendu le site injoignable. Regle posee : le renumerotage declaratif vient APRES la derniere operation qui a besoin de l ancienne infrastructure. nftables_admin_ssh etait une liste a la main qui devait suivre le plan d administration. Elle a pris du retard le jour meme. Le plan de frontiere n a propose AUCUNE creation ni suppression de regle — seulement douze contenus d alias. Les regles visent des alias par leur nom : un renumerotage complet se reduit a changer ce que les noms designent. Routes des trois hyperviseurs refaites, declarees ET vives, avec sauvegarde de /etc/network/interfaces. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-12 16:32:06 -04:00
site-raser: ansible-runtime ## DESTRUCTIF : detruit les VM du SITE — CONFIRMER=true ET SITE=<depot>
@# CETTE CIBLE MANQUAIT, ET SON ABSENCE AVAIT UN NOM (2026-09-12). `raser` ne vise que
@# l'instance ACTIVE — un locataire. Rien ne detruisait les machines du site : ce n'est
@# donc pas que personne n'avait essaye de le reconstruire depuis zero, c'est que
@# l'outil n'en offrait pas le moyen. La limite « le site n'a jamais ete reconstruit »
@# se lisait partout sans que sa CAUSE soit nommee.
@#
@# LE SITE PORTE LE DEPOT DE SAUVEGARDE DE CHAQUE LOCATAIRE et la racine de son AC.
@# `make depot-hors-site VERS=<repertoire>` AVANT, toujours.
python3 scripts/site_raser.py $(if $(filter true,$(CONFIRMER)),--confirmer) \
$(if $(SITE),--site "$(SITE)") $(if $(MACHINE),--machine "$(MACHINE)")
.PHONY: site-raser
site-creer: ## Cree les machines du site depuis l'underlay — CONFIRMER=true
@set -e; \
if [[ "$(CONFIRMER)" != "true" ]]; then \
printf '%s\n' 'Refus: creation des VM de l hebergeur (clone Proxmox).'; \
printf '%s\n' 'Relancer avec CONFIRMER=true.'; \
exit 2; \
fi; \
machines="$$(python3 scripts/site_machines.py lister)"; \
if [[ -z "$$machines" ]]; then printf '%s\n' 'Refus: aucune machine declaree dans l underlay.'; exit 2; fi; \
for m in $$machines; do \
printf '\n=== Machine du site: %s ===\n' "$$m"; \
clonage : quatre defauts que la premiere VM placee hors du noeud du gabarit a reveles Toutes les VM naissaient sur le noeud du gabarit puis migraient. site-cache-01 est la premiere a etre placee ailleurs, et elle a fait tomber quatre defauts enchaines : 1. l'URL de clonage visait le noeud de DESTINATION, alors que l'API veut celui qui DETIENT le gabarit ; la destination se dit par `target`. D'ou un 500 sur tout clonage inter-noeuds. Le noeud du gabarit se decouvre dans l'inventaire du cluster. 2. ce 500 etait avale par failed_when: false + no_log: true. Une assertion le releve desormais, message de l'API compris. 3. l'attente interrogeait nodes/<destination>/tasks/<UPID> ; la tache vit sur le noeud du gabarit. 60 tentatives x 10 s pour un clonage termine en 87 s, puis la suite qui reprend sans un mot. Le noeud se lit dans l'UPID lui-meme. 4. la garde d'apres-attente retombait sur `exitstatus | default('OK')` : une attente qui n'avait rien observe passait pour un succes. Elle exige d'avoir VU la tache s'arreter. Et une taille de disque porte toujours son unite : `disque: 40` etait lu comme un retrecissement, erreur toleree a raison — la machine naissait donc avec les 16 Go du gabarit au lieu de 40, sans que rien ne le dise. Le site declare enfin ce qu'il materialise (materialisation: gabarit, stockage, clone_complet) au lieu de l'heriter du tenant actif. Chrono : clonage reel 59 s et 87 s pour 3,3 Gio alloues. 42 preuves vertes, ansible-lint profil production. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 02:44:21 -04:00
t0=$$(date +%s); \
params="$$(python3 scripts/site_machines.py parametres-proxmox --machine "$$m")"; \
eval "$$params"; \
$(MAKE) cloner-vm \
pools : le genome ne nait plus chez un locataire `Chezlepro-17` contenait VINGT ET UNE VM : les quatorze du locataire ET les sept du genome. `OPS-Chezlepro` et `OPS-Technolibre`, crees a la main, etaient vides. La cause : `site-creer` appelle `cloner-vm`, qui derive son pool par `--pool-actif`, c est-a-dire le pool du TENANT lie. Les machines du site heritaient du locataire courant. Range a la main, ca se serait defait au prochain `site-creer` — sans un mot, parce que la VM est bien creee, bien nommee, bien adressee. Seule son appartenance est fausse, et rien ne la regarde. - `--pool-site` rend le nom invariable du pool du genome. Option DISTINCTE, pas un drapeau sur la premiere : une fonction qui repond aux deux questions finit par se tromper d appelant. - `cloner-vm` accepte une surcharge POOL= ; `site-creer` la nomme. Substitution au niveau MAKE, pas shell : POOL arrive du sur-make comme variable make, et $${POOL} ne l aurait jamais vue. - P71 exige que `site-creer` nomme son pool. Eprouvee dans les deux sens : passe sur le Makefile sain, tire des qu on retire l argument. Applique au cluster : Site-OPS 9 VM (7 du site + 2 gabarits), OPS-Chezlepro 14, OPS-Patient0 5. Chezlepro-17, Patient0-29 et Set-OPS supprimes une fois vides. Les pools anterieurs a Set-OPS et les quinze VM hors pool n ont pas ete touches. make prouver : 70 OK, 0 echec, 1 saute. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-13 04:12:27 -04:00
POOL="$$(python3 scripts/devis_proxmox_pools.py --pool-site)" \
HOTE="$$m" \
VMID="$$SETOPS_VMID" \
ADRESSE_IP="$$SETOPS_IP" \
CIDR="$$SETOPS_CIDR" \
PASSERELLE="$$SETOPS_PASSERELLE" \
VLAN="$${SETOPS_VLAN:-}" \
PONT_PROXMOX="$$SETOPS_PONT" \
STOCKAGE_PROXMOX="$${SETOPS_STOCKAGE:-$(STOCKAGE_PROXMOX)}" \
TAILLE_DISQUE="$$SETOPS_DISQUE" \
COEURS="$$SETOPS_COEURS" \
MEMOIRE="$$SETOPS_MEMOIRE" \
MEMOIRE_MIN="$$SETOPS_MEMOIRE_MIN" \
NOEUD_PROXMOX="$$SETOPS_NOEUD" \
gabarit : refabrique, minimal, et puise aux ressources du SITE REFABRIQUE (VMID 9006, modeleSetOPS-minimal). Quatre roles au lieu de dix-sept : qemu_guest_agent, cloud_init, sudo_ansible, ssh_baseline — des conditions d existence, pas des choix d efficacite. Tout le reste vient du socle, et P56 refuse qu un role retire ne soit repris par personne. FABRIQUE CHEZ LE SITE, ET C EST LA DECISION QUI COMPTE. « Les ressources du SITE font autorite pour tous ses artefacts ; elles servent les tenants jusqu a ce qu ils s emancipent. » Il se fabriquait DEHORS : `-i "<ip>,"` ne porte aucun group_vars, donc ni mandataire ni resolveur. L ancien gabarit allait chercher ses paquets chez Debian et resolvait chez l ancien LAN — 192.168.10.10, lu sur la VM 99998. Le site avait son cache et son resolveur, et son propre artefact les ignorait. Desormais la fabrication DERIVE ses ressources du plan du site (underlay --adresses) et tourne sur le reseau du genome. PROUVE : dix requetes de 10.0.33.31 servies par site-cache-01, resolveur pose a 10.0.34.11. L IDENTITE DU GABARIT VIENT DU SITE, PLUS DU TENANT. `proxmox_clone_vmid_modele` vivait dans les group_vars de l ecosysteme : deux tenants pouvaient cloner deux gabarits differents sans que rien ne le dise, et un tenant decidait d un objet dont descend chaque VM de chaque ecosysteme. Meme mouvement que l INDEX (2026-08-25) : le site ALLOUE, le tenant RECOIT. Cle `gabarit` du plan du site, lue par underlay --gabarit. PIEGE FERME EN CHEMIN : creer-vm passait VMID_MODELE="$SETOPS_VMID_MODELE" a cloner-vm, or inventory_host.py n emet PAS cette variable. Elle valait donc le VIDE, et ce vide ECRASAIT la valeur derivee — le gabarit du tenant reprenait la main sans bruit. ET UN PIEGE DEJA DOCUMENTE, PAYE UNE TROISIEME FOIS : le chemin du controleur contient une espace, et `lookup('pipe', ...)` le decoupait. Guillemets. EPROUVE DE BOUT EN BOUT : VM clonee du gabarit minimal — nom, adresse, machine-id neuf, cle d hote regeneree, agent invite actif ; puis socle applique dessus, changed=5, 0 echec, auditd compris. VM d essai retiree. make verifier : vert. make prouver : CONFORME, 56 OK, 0 echec. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-09-01 13:46:31 -04:00
VMID_MODELE="$${SETOPS_VMID_MODELE:-$(VMID_MODELE)}" \
clonage : quatre defauts que la premiere VM placee hors du noeud du gabarit a reveles Toutes les VM naissaient sur le noeud du gabarit puis migraient. site-cache-01 est la premiere a etre placee ailleurs, et elle a fait tomber quatre defauts enchaines : 1. l'URL de clonage visait le noeud de DESTINATION, alors que l'API veut celui qui DETIENT le gabarit ; la destination se dit par `target`. D'ou un 500 sur tout clonage inter-noeuds. Le noeud du gabarit se decouvre dans l'inventaire du cluster. 2. ce 500 etait avale par failed_when: false + no_log: true. Une assertion le releve desormais, message de l'API compris. 3. l'attente interrogeait nodes/<destination>/tasks/<UPID> ; la tache vit sur le noeud du gabarit. 60 tentatives x 10 s pour un clonage termine en 87 s, puis la suite qui reprend sans un mot. Le noeud se lit dans l'UPID lui-meme. 4. la garde d'apres-attente retombait sur `exitstatus | default('OK')` : une attente qui n'avait rien observe passait pour un succes. Elle exige d'avoir VU la tache s'arreter. Et une taille de disque porte toujours son unite : `disque: 40` etait lu comme un retrecissement, erreur toleree a raison — la machine naissait donc avec les 16 Go du gabarit au lieu de 40, sans que rien ne le dise. Le site declare enfin ce qu'il materialise (materialisation: gabarit, stockage, clone_complet) au lieu de l'heriter du tenant actif. Chrono : clonage reel 59 s et 87 s pour 3,3 Gio alloues. 42 preuves vertes, ansible-lint profil production. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 02:44:21 -04:00
CLONE_COMPLET="$$SETOPS_CLONE_COMPLET" \
FORMAT_DISQUE="$(FORMAT_DISQUE)"; \
clonage : quatre defauts que la premiere VM placee hors du noeud du gabarit a reveles Toutes les VM naissaient sur le noeud du gabarit puis migraient. site-cache-01 est la premiere a etre placee ailleurs, et elle a fait tomber quatre defauts enchaines : 1. l'URL de clonage visait le noeud de DESTINATION, alors que l'API veut celui qui DETIENT le gabarit ; la destination se dit par `target`. D'ou un 500 sur tout clonage inter-noeuds. Le noeud du gabarit se decouvre dans l'inventaire du cluster. 2. ce 500 etait avale par failed_when: false + no_log: true. Une assertion le releve desormais, message de l'API compris. 3. l'attente interrogeait nodes/<destination>/tasks/<UPID> ; la tache vit sur le noeud du gabarit. 60 tentatives x 10 s pour un clonage termine en 87 s, puis la suite qui reprend sans un mot. Le noeud se lit dans l'UPID lui-meme. 4. la garde d'apres-attente retombait sur `exitstatus | default('OK')` : une attente qui n'avait rien observe passait pour un succes. Elle exige d'avoir VU la tache s'arreter. Et une taille de disque porte toujours son unite : `disque: 40` etait lu comme un retrecissement, erreur toleree a raison — la machine naissait donc avec les 16 Go du gabarit au lieu de 40, sans que rien ne le dise. Le site declare enfin ce qu'il materialise (materialisation: gabarit, stockage, clone_complet) au lieu de l'heriter du tenant actif. Chrono : clonage reel 59 s et 87 s pour 3,3 Gio alloues. 42 preuves vertes, ansible-lint profil production. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 02:44:21 -04:00
printf '=== %s : %s s ===\n' "$$m" "$$(( $$(date +%s) - t0 ))"; \
done; \
printf '\nLes machines du site sont creees.\n'
.PHONY: _attendre-flotte
_attendre-flotte: ansible-runtime
@set -e; \
max="$${ATTENTE_MAX:-600}"; deadline=$$(( $$(date +%s) + max )); \
printf 'Attente que la flotte reponde en SSH (max %ss)...\n' "$$max"; \
until ansible -i $(INVENTAIRE_PRODUCTION) $(GROUPE_HOTES_ACTIFS) -m ping -e ansible_become=false >/dev/null 2>&1; do \
if (( $$(date +%s) > deadline )); then printf 'Timeout: flotte injoignable apres %ss.\n' "$$max"; exit 1; fi; \
sleep 10; \
done; \
printf 'Flotte joignable.\n'
.PHONY: reconstruire
raser: ansible-runtime _instance-requise ## DESTRUCTIF : detruit les VM derivees du plan — exige CONFIRMER=true ET INSTANCE=<nom>
@python3 scripts/raser.py $(if $(INSTANCE),--instance $(INSTANCE)) $(if $(HOTE),--hote $(HOTE)) $(if $(filter true,$(CONFIRMER)),--confirmer)
portabilite : le repli nftables survivait a la bascule et annulait tout Le socle pose l'un de DEUX fichiers dans /etc/nftables.conf : le ruleset derive (table setops_flux) s'il existe, sinon un gabarit de repli plat (table setops_filter) qui n'ouvre que le 22. Ce sont des ALTERNATIVES, jamais des couches. Mais le rechargement est `nft -f`, qui AJOUTE sans purger, et le fichier derive retirait setops_flux — jamais setops_filter. Un hote passe du repli au derive gardait donc DEUX chaines input sur le meme hook, toutes deux en policy drop. Le paquet traverse les deux : seule l'intersection de leurs accept passait. Symptome parfaitement trompeur : AC debout, 8443 en ecoute, regle posee et acceptante, ICMP entre les deux VM a 0,15 ms — et step ca bootstrap qui expire. Il a fallu lire le ruleset entier pour voir la seconde table. Le fichier derive retire desormais les DEUX tables (le repli portait deja flush ruleset). Pas de flush ruleset cote derive : choix du depot pour ne pas detruire de tables etrangeres, respecte. Ce qui l'avait rendu possible : `make reconstruire` ne generait JAMAIS les flux. `make flux` en est maintenant la premiere etape. Et `no_log` a masque la cause trois fois dans la journee — dont au terme d'un deploiement de 157 taches. Les taches concernees extraient desormais le verdict a part (ce qui a echoue, quel code, quel message) sans toucher aux en-tetes. Une garde qui protege un secret ne doit pas emporter le diagnostic. Verifie : TLS OK depuis infra-dns-01 vers l'AC, une seule table nftables, 13/14 hotes deployes sans echec, prouver.py 34 OK, ansible-lint production. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 12:24:41 -04:00
# `make flux` D'ABORD, et ce n'est pas une precaution : sans lui, `instance/flux-genere/`
# est VIDE et le socle deploie nftables en `policy drop` SANS AUCUNE REGLE DERIVEE. La
# flotte se monte, SSH repond depuis l'administration — et tout le reste est mur.
#
# Invisible sur un ecosysteme deja construit, dont les .nft trainaient d'une execution
# precedente. Trouve le 2026-08-10 en montant Technolibre depuis zero : l'AC etait
# debout, son port 8443 en ecoute, et `step ca bootstrap` expirait depuis une VM du
# meme sous-reseau. Zero regle pour 8443 dans le ruleset, `flux-genere/` vide.
reconstruire: _instance-requise ## Reconstruit un ecosysteme depuis zero : VM puis deploiement complet
@set -e; \
if [[ "$(CONFIRMER)" != "true" ]]; then \
printf '%s\n' 'Refus: RECONSTRUCTION — cree les VM manquantes (2a) PUIS deploie tout (2b).'; \
printf '%s\n' 'Idempotent : une VM deja presente est sautee (clone par nom), le deploiement converge.'; \
printf '%s\n' 'Relancer avec CONFIRMER=true.'; \
exit 2; \
fi; \
portabilite : le repli nftables survivait a la bascule et annulait tout Le socle pose l'un de DEUX fichiers dans /etc/nftables.conf : le ruleset derive (table setops_flux) s'il existe, sinon un gabarit de repli plat (table setops_filter) qui n'ouvre que le 22. Ce sont des ALTERNATIVES, jamais des couches. Mais le rechargement est `nft -f`, qui AJOUTE sans purger, et le fichier derive retirait setops_flux — jamais setops_filter. Un hote passe du repli au derive gardait donc DEUX chaines input sur le meme hook, toutes deux en policy drop. Le paquet traverse les deux : seule l'intersection de leurs accept passait. Symptome parfaitement trompeur : AC debout, 8443 en ecoute, regle posee et acceptante, ICMP entre les deux VM a 0,15 ms — et step ca bootstrap qui expire. Il a fallu lire le ruleset entier pour voir la seconde table. Le fichier derive retire desormais les DEUX tables (le repli portait deja flush ruleset). Pas de flush ruleset cote derive : choix du depot pour ne pas detruire de tables etrangeres, respecte. Ce qui l'avait rendu possible : `make reconstruire` ne generait JAMAIS les flux. `make flux` en est maintenant la premiere etape. Et `no_log` a masque la cause trois fois dans la journee — dont au terme d'un deploiement de 157 taches. Les taches concernees extraient desormais le verdict a part (ce qui a echoue, quel code, quel message) sans toucher aux en-tetes. Une garde qui protege un secret ne doit pas emporter le diagnostic. Verifie : TLS OK depuis infra-dns-01 vers l'AC, une seule table nftables, 13/14 hotes deployes sans echec, prouver.py 34 OK, ansible-lint production. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 12:24:41 -04:00
$(MAKE) flux; \
$(MAKE) flotte-creer CONFIRMER=true; \
$(MAKE) _attendre-flotte; \
$(MAKE) _amorcer-socle; \
$(MAKE) deployer-tout CONFIRMER=true
# « Go ahead, make my day. » — LE bouton rouge : alias de reconstruire (Clint Eastwood).
# Cree toutes les VM puis deploie tout, en une commande. Garde CONFIRMER=true.
.PHONY: myDay
# « Une PKI et un DNS fonctionnels avant toute chose » — contrainte d'architecture du
# 2026-08-08. `deployer-tout` deroule par COUCHES : tous les hotes recoivent le socle,
# puis tous recoivent la couche suivante. C'est correct, mais ca laisse chaque VM
# reclamer un certificat a une autorite qui n'est pas encore debout — et l'echec se lit
# alors comme un defaut du role, pas comme un defaut d'ordre.
#
# On monte donc l'AC puis le DNS COMPLETEMENT d'abord, hote par hote (`make deployer`
# ordonne deja les groupes d'un hote par couches). Les deux se DERIVENT du plan :
# deplacer l'autorite deplace l'amorcage avec elle.
_amorcer-socle: ansible-runtime _instance-requise
@set -e; \
for h in $$(python3 scripts/socle_amorcage.py); do \
printf '\n=== Amorcage du socle : %s ===\n' "$$h"; \
$(MAKE) deployer HOTE="$$h"; \
done; \
printf '\nPKI et DNS debout : le reste de la flotte peut monter.\n'
myDay: reconstruire ## Alias strict de `reconstruire` (meme cible, memes gardes)
deployer-groupe: ## Deploie un seul groupe sur toute la flotte — GROUPE=<groupe>
@if [[ -z "$(GROUPE)" ]]; then \
printf '%s\n' 'Refus: relancer avec GROUPE=nom_groupe.'; \
exit 2; \
fi
$(MAKE) appliquer GROUPE="$(GROUPE)"
.PHONY: verifier-deploiement
verifier-deploiement: ansible-runtime ## Verifie l'etat de la flotte apres deploiement
@set -e; \
if [[ -z "$(HOTE)" ]]; then \
printf '%s\n' 'Refus: relancer avec HOTE=nom_hote.'; \
exit 2; \
fi; \
python3 scripts/inventory_host.py --inventaire $(FICHIER_INVENTAIRE) verifier-actif --hote $(HOTE); \
python3 scripts/inventory_host.py --inventaire $(FICHIER_INVENTAIRE) --dependances $(FICHIER_DEPENDANCES) verifier-dependances-hote --hote $(HOTE); \
playbooks="$$(python3 scripts/inventory_host.py --inventaire $(FICHIER_INVENTAIRE) playbooks --hote $(HOTE) --dossier-playbooks $(DOSSIER_PLAYBOOKS_GROUPES))"; \
if [[ -z "$$playbooks" ]]; then \
printf '%s\n' 'Refus: aucun playbook applicable pour HOTE=$(HOTE).'; \
exit 2; \
fi; \
vault_chiffre="$$(grep -rlsIF '$$ANSIBLE_VAULT' $(dir $(INVENTAIRE_PRODUCTION))group_vars 2>/dev/null | head -1 || true)"; \
voutes : une voute, une cle — separer avant de distribuer Decision de l'exploitant : chaque runner est maitre de sa voute et en detient la cle. C'est ce qui rend un runner autonome, donc ce qui rend l'emancipation atteignable. Elle exigeait un prealable, mesure ce matin : UN SEUL mot de passe ouvrait les SIX voutes de la flotte, celle de la fabric comprise. DISTRIBUER AVANT DE SEPARER AURAIT ETE PIRE QUE LE STATU QUO : poser « la » cle sur chaque runner rendait chaque runner capable d'ouvrir les autres. Compromettre le plus petit locataire donnait les secrets de l'hebergeur. Cinq cles, une par ecosysteme, sous ~/.config/setops-vault-<depot>. Verification croisee apres rechiffrement : la diagonale, et rien qu'elle. L'ancienne cle maitresse n'ouvre plus aucune des six. UN SEUL MOT DE PASSE NE POUVAIT PLUS SUFFIRE, et pas pour la raison qu'on croit : `cloner_vm_debian.yml` charge la voute du TENANT puis celle de l'UNDERLAY dans la meme execution. `ANSIBLE_VAULT_IDENTITY_LIST` en porte plusieurs et les essaie toutes — un seul export suffit pour les 28 appels a ansible-playbook, sans en toucher un seul. La liste se derive dans scripts/voutes.py. CE QUI BORNE LE POUVOIR N'EST PAS LA LISTE MAIS LA PRESENCE DES FICHIERS. Sur le poste, toutes les cles sont la — c'est l'humain qui les detient toutes, et P03 lit les inventaires de tous les freres. Sur un runner, une seule existe. Le code est identique, le pouvoir ne l'est pas. DEUX PREUVES ONT DIT CE QUI MANQUAIT. P03 est tombee des la separation : elle lit les inventaires voisins, donc il lui faut leurs cles — c'est elle qui a etabli que la liste devait couvrir le voisinage. P16 s'est mise a SAUTER : sa garde ne connaissait que ANSIBLE_VAULT_PASSWORD_FILE. Une preuve sautee se lit trop facilement comme une preuve passee. COUT ASSUME : le chiffre et sa cle cohabiteront sur la meme machine des que les runners recevront la leur. Le pari tient parce que le perimetre est borne. Les six voutes sauvegardees avant rechiffrement. L'ancienne cle maitresse reste sur le poste : la retirer est une decision, pas un nettoyage. make verifier : vert. make prouver : CONFORME, 52 OK, 0 echec, 0 saute — sans ANSIBLE_VAULT_PASSWORD_FILE. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-08-28 14:23:33 -04:00
if [[ -n "$$vault_chiffre" && -z "$${ANSIBLE_VAULT_PASSWORD_FILE:-}$${ANSIBLE_VAULT_IDENTITY_LIST:-}" ]]; then \
if [[ -t 0 ]]; then \
read -r -s -p 'Mot de passe du vault Ansible: ' mdp; echo; \
vf="$$(mktemp)"; printf '%s' "$$mdp" > "$$vf"; chmod 600 "$$vf"; \
export ANSIBLE_VAULT_PASSWORD_FILE="$$vf"; \
trap 'rm -f "$$vf"' EXIT; \
else \
printf '%s\n' 'Refus: vault chiffre detecte mais aucun mot de passe (entree non interactive). Fournir ANSIBLE_VAULT_PASSWORD_FILE ou le champ vault de la GUI.'; \
exit 2; \
fi; \
fi; \
for playbook in $$playbooks; do \
ansible-playbook -i $(INVENTAIRE_PRODUCTION) "$$playbook" --limit "$(HOTE)" --check --diff; \
done
cloner-vm: ansible-runtime ## Clone une VM depuis le gabarit dore — HOTE=<nom> VMID=<id>
@if [[ -z "$(HOTE)" || -z "$(VMID)" ]]; then \
printf '%s\n' 'Refus: relancer avec HOTE=nom VMID=id_clone.'; \
exit 2; \
fi
@# En SDN EVPN, l'etiquette est portee par le VNet : `instancier` emet donc un VLAN
@# VIDE et un pont derive (t17serv). Exiger un VLAN ici rejetait toute VM de tenant.
@# Un VLAN vide n'est accepte QUE si un pont est fourni — sinon la VM n'aurait ni
@# etiquette ni VNet, et se retrouverait branchee nulle part.
@if [[ -z "$(VLAN)" && -z "$(PONT_PROXMOX)" ]]; then \
printf '%s\n' 'Refus: relancer avec VLAN=id_vlan, ou PONT_PROXMOX=<vnet> en SDN.'; \
exit 2; \
fi
@if [[ -n "$(VLAN)" ]] && { ! [[ "$(VLAN)" =~ ^[0-9]+$$ ]] || (( 10#$(VLAN) < 1 || 10#$(VLAN) > 4094 )); }; then \
printf '%s\n' 'Refus: VLAN doit etre un nombre entre 1 et 4094.'; \
exit 2; \
fi
@if [[ -n "$(CLE_SSH_PUBLIQUE)" && ! -f "$(CLE_SSH_PUBLIQUE)" ]]; then \
printf '%s\n' 'Refus: cle publique SSH introuvable: $(CLE_SSH_PUBLIQUE)'; \
exit 2; \
fi
@if [[ "$(DHCP)" != "true" && ( -z "$(ADRESSE_IP)" || -z "$(CIDR)" || -z "$(PASSERELLE)" ) ]]; then \
printf '%s\n' 'Refus: fournir ADRESSE_IP, CIDR et PASSERELLE, ou utiliser DHCP=true.'; \
exit 2; \
fi
@ipconfig='ip=dhcp'; \
if [[ "$(DHCP)" != "true" ]]; then \
ipconfig='ip=$(ADRESSE_IP)/$(CIDR),gw=$(PASSERELLE)'; \
fi; \
extra_vars=( \
-e proxmox_clone_nom="$(HOTE)" \
-e proxmox_clone_vmid="$(VMID)" \
-e proxmox_clone_ipconfig0="$$ipconfig" \
pools : le genome ne nait plus chez un locataire `Chezlepro-17` contenait VINGT ET UNE VM : les quatorze du locataire ET les sept du genome. `OPS-Chezlepro` et `OPS-Technolibre`, crees a la main, etaient vides. La cause : `site-creer` appelle `cloner-vm`, qui derive son pool par `--pool-actif`, c est-a-dire le pool du TENANT lie. Les machines du site heritaient du locataire courant. Range a la main, ca se serait defait au prochain `site-creer` — sans un mot, parce que la VM est bien creee, bien nommee, bien adressee. Seule son appartenance est fausse, et rien ne la regarde. - `--pool-site` rend le nom invariable du pool du genome. Option DISTINCTE, pas un drapeau sur la premiere : une fonction qui repond aux deux questions finit par se tromper d appelant. - `cloner-vm` accepte une surcharge POOL= ; `site-creer` la nomme. Substitution au niveau MAKE, pas shell : POOL arrive du sur-make comme variable make, et $${POOL} ne l aurait jamais vue. - P71 exige que `site-creer` nomme son pool. Eprouvee dans les deux sens : passe sur le Makefile sain, tire des qu on retire l argument. Applique au cluster : Site-OPS 9 VM (7 du site + 2 gabarits), OPS-Chezlepro 14, OPS-Patient0 5. Chezlepro-17, Patient0-29 et Set-OPS supprimes une fois vides. Les pools anterieurs a Set-OPS et les quinze VM hors pool n ont pas ete touches. make prouver : 70 OK, 0 echec, 1 saute. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-13 04:12:27 -04:00
-e proxmox_clone_pool="$(if $(POOL),$(POOL),$$(python3 scripts/devis_proxmox_pools.py --pool-actif))" \
); \
[[ -n "$(VMID_MODELE)" ]] && extra_vars+=( -e proxmox_clone_vmid_modele="$(VMID_MODELE)" ); \
[[ -n "$(NOEUD_PROXMOX)" ]] && extra_vars+=( -e proxmox_clone_noeud="$(NOEUD_PROXMOX)" ); \
[[ -n "$(STOCKAGE_PROXMOX)" ]] && extra_vars+=( -e proxmox_clone_stockage="$(STOCKAGE_PROXMOX)" ); \
[[ -n "$(FORMAT_DISQUE)" ]] && extra_vars+=( -e proxmox_clone_format="$(FORMAT_DISQUE)" ); \
[[ -n "$(CLONE_COMPLET)" ]] && extra_vars+=( -e proxmox_clone_complet="$(CLONE_COMPLET)" ); \
[[ -n "$(TAILLE_DISQUE)" ]] && extra_vars+=( -e proxmox_clone_taille_disque="$(TAILLE_DISQUE)" ); \
[[ -n "$(COEURS)" ]] && extra_vars+=( -e proxmox_clone_coeurs="$(COEURS)" ); \
[[ -n "$(MEMOIRE)" ]] && extra_vars+=( -e proxmox_clone_memoire="$(MEMOIRE)" ); \
[[ -n "$(MEMOIRE_MIN)" ]] && extra_vars+=( -e proxmox_clone_memoire_min="$(MEMOIRE_MIN)" ); \
[[ -n "$(DISQUE_PROXMOX)" ]] && extra_vars+=( -e proxmox_clone_disque="$(DISQUE_PROXMOX)" ); \
[[ -n "$(DNS)" ]] && extra_vars+=( -e proxmox_clone_dns="$(DNS)" ); \
[[ -n "$(DOMAINE_RECHERCHE)" ]] && extra_vars+=( -e proxmox_clone_domaines_recherche="$(DOMAINE_RECHERCHE)" ); \
[[ -n "$(CIUSER)" ]] && extra_vars+=( -e proxmox_clone_ciuser="$(CIUSER)" ); \
[[ -n "$(CLE_SSH_PUBLIQUE)" ]] && extra_vars+=( -e proxmox_clone_cle_publique_fichier="$(CLE_SSH_PUBLIQUE)" ); \
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
[[ -n "$${SETOPS_CLES_AMORCAGE:-}" ]] && extra_vars+=( -e "$$(python3 -c 'import json,os; print(json.dumps({"proxmox_clone_cles_amorcage": os.environ["SETOPS_CLES_AMORCAGE"]}))')" ); \
[[ -n "$(PONT_PROXMOX)" ]] && extra_vars+=( -e proxmox_clone_pont="$(PONT_PROXMOX)" ); \
extra_vars+=( -e proxmox_clone_vlan="$(VLAN)" ); \
[[ -n "$(DEMARRER)" ]] && extra_vars+=( -e proxmox_clone_demarrer="$(DEMARRER)" ); \
vault_args=(); \
vault_file=""; \
for d in lab principal production; do \
for gv in group_vars/all/vault.yml group_vars/proxmox.vault.yml; do \
f="$(SETOPS_INSTANCE)/inventories/$$d/$$gv"; \
[[ -f "$$f" ]] && vault_file="$$f" && break 2; \
done; \
done; \
voutes : le monde physique a la sienne, les tenants n'en portent plus les cles Demande de l'exploitant : « l'underlay et son tenant doivent avoir chacun sa voute ». CE QUI ETAIT FAUX. Le jeton d'API du cluster et la cle d'API de la frontiere vivaient dans la voute de CHAQUE tenant. Patient 0 a du les recopier pour exister. Consequence : on ne pouvait plus revoquer l'acces d'un locataire sans le revoquer pour tous — la faute des neuf copies, appliquee aux secrets. CE QUI EST POSE : - `underlay.vault.yml`, chez l'hebergeur, a cote d'underlay.yml. Quatre secrets deplaces (jeton Proxmox, cle et secret d'API OPNsense), retires des deux voutes de tenants. - `proxmox_api.voute()` lit l'underlay APRES le tenant, donc l'hebergeur fait foi ; un site non encore migre continue de fonctionner sur son ancienne voute. - `appliquer_opnsense._voute()` n'a plus sa propre lecture : elle appelle celle du cluster. La reecrire aurait fait une dixieme copie le jour ou l'on refermait les neuf autres. - Le playbook de clonage charge la voute de l'underlay APRES celle du tenant, par la meme derivation que proxmox-hebergeur.yml : le symlink designe deja l'hebergeur. - `voute.py` sait que ces secrets ne sont plus attendus chez un tenant (P18). EPROUVE SUR LE REEL, apres retrait des cles chez les deux tenants : l'API du cluster repond, le devis de placement est conforme pour les deux ecosystemes, le devis de frontiere se genere (86 objets), le SDN est convergent. UNE PRECAUTION APPRISE EN CHEMIN : le premier essai a ecrit la voute EN CLAIR avant de la chiffrer, et le chiffrement a echoue — il a fallu detruire le fichier. La sequence est desormais l'inverse : chiffrer dans un dossier de travail, ne deposer que le resultat. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 17:13:48 -04:00
if [[ -z "$$vault_file" && -L underlay.yml ]]; then \
vault_file="$$(dirname "$$(readlink -f underlay.yml)")/underlay.vault.yml"; \
fi; \
if [[ -n "$$vault_file" && -f "$$vault_file" ]]; then \
read -r premiere_ligne < "$$vault_file" || true; \
case "$$premiere_ligne" in \
'$$ANSIBLE_VAULT'*) \
voutes : une voute, une cle — separer avant de distribuer Decision de l'exploitant : chaque runner est maitre de sa voute et en detient la cle. C'est ce qui rend un runner autonome, donc ce qui rend l'emancipation atteignable. Elle exigeait un prealable, mesure ce matin : UN SEUL mot de passe ouvrait les SIX voutes de la flotte, celle de la fabric comprise. DISTRIBUER AVANT DE SEPARER AURAIT ETE PIRE QUE LE STATU QUO : poser « la » cle sur chaque runner rendait chaque runner capable d'ouvrir les autres. Compromettre le plus petit locataire donnait les secrets de l'hebergeur. Cinq cles, une par ecosysteme, sous ~/.config/setops-vault-<depot>. Verification croisee apres rechiffrement : la diagonale, et rien qu'elle. L'ancienne cle maitresse n'ouvre plus aucune des six. UN SEUL MOT DE PASSE NE POUVAIT PLUS SUFFIRE, et pas pour la raison qu'on croit : `cloner_vm_debian.yml` charge la voute du TENANT puis celle de l'UNDERLAY dans la meme execution. `ANSIBLE_VAULT_IDENTITY_LIST` en porte plusieurs et les essaie toutes — un seul export suffit pour les 28 appels a ansible-playbook, sans en toucher un seul. La liste se derive dans scripts/voutes.py. CE QUI BORNE LE POUVOIR N'EST PAS LA LISTE MAIS LA PRESENCE DES FICHIERS. Sur le poste, toutes les cles sont la — c'est l'humain qui les detient toutes, et P03 lit les inventaires de tous les freres. Sur un runner, une seule existe. Le code est identique, le pouvoir ne l'est pas. DEUX PREUVES ONT DIT CE QUI MANQUAIT. P03 est tombee des la separation : elle lit les inventaires voisins, donc il lui faut leurs cles — c'est elle qui a etabli que la liste devait couvrir le voisinage. P16 s'est mise a SAUTER : sa garde ne connaissait que ANSIBLE_VAULT_PASSWORD_FILE. Une preuve sautee se lit trop facilement comme une preuve passee. COUT ASSUME : le chiffre et sa cle cohabiteront sur la meme machine des que les runners recevront la leur. Le pari tient parce que le perimetre est borne. Les six voutes sauvegardees avant rechiffrement. L'ancienne cle maitresse reste sur le poste : la retirer est une decision, pas un nettoyage. make verifier : vert. make prouver : CONFORME, 52 OK, 0 echec, 0 saute — sans ANSIBLE_VAULT_PASSWORD_FILE. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-08-28 14:23:33 -04:00
if [[ -z "$${ANSIBLE_VAULT_PASSWORD_FILE:-}$${ANSIBLE_VAULT_IDENTITY_LIST:-}" ]]; then \
vault_args+=( --ask-vault-pass ); \
fi; \
;; \
esac; \
fi; \
ansible-playbook -i localhost, $(PLAYBOOK_PROXMOX_CLONER_VM) "$${vault_args[@]}" "$${extra_vars[@]}"
creer-vm: _instance-requise ## Cree une VM et attend qu'elle soit joignable — HOTE=<nom>
@set -e; \
if [[ -z "$(HOTE)" ]]; then \
printf '%s\n' 'Refus: relancer avec HOTE=nom_hote (declare dans le plan).'; \
exit 2; \
fi; \
params="$$(python3 scripts/inventory_host.py --inventaire $(INVENTAIRE_PRODUCTION) parametres-proxmox --hote $(HOTE))"; \
eval "$$params"; \
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
export SETOPS_CLES_AMORCAGE; \
$(MAKE) cloner-vm \
HOTE="$(HOTE)" \
VMID="$$SETOPS_VMID" \
ADRESSE_IP="$$SETOPS_IP" \
CIDR="$$SETOPS_CIDR" \
PASSERELLE="$$SETOPS_PASSERELLE" \
VLAN="$$SETOPS_VLAN" \
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
PONT_PROXMOX="$${SETOPS_PONT:-$(PONT_PROXMOX)}" \
STOCKAGE_PROXMOX="$${SETOPS_STOCKAGE:-$(STOCKAGE_PROXMOX)}" \
TAILLE_DISQUE="$${SETOPS_DISQUE:-$(TAILLE_DISQUE)}" \
COEURS="$${SETOPS_COEURS:-$(COEURS)}" \
MEMOIRE="$${SETOPS_MEMOIRE:-$(MEMOIRE)}" \
MEMOIRE_MIN="$${SETOPS_MEMOIRE_MIN:-$(MEMOIRE_MIN)}" \
NOEUD_PROXMOX="$${SETOPS_NOEUD:-$(NOEUD_PROXMOX)}" \
VMID_MODELE="$(VMID_MODELE)" \
FORMAT_DISQUE="$(FORMAT_DISQUE)" \
DISQUE_PROXMOX="$(DISQUE_PROXMOX)" \
DNS="$${SETOPS_DNS:-$(DNS)}" \
DOMAINE_RECHERCHE="$${SETOPS_DOMAINE:-$(DOMAINE_RECHERCHE)}" \
DHCP="$(DHCP)" \
CIUSER="$(CIUSER)" \
CLE_SSH_PUBLIQUE="$(CLE_SSH_PUBLIQUE)" \
DEMARRER="$(DEMARRER)" \
CLONE_COMPLET="$(CLONE_COMPLET)"
creer-vm : prouver la materialisation sans entrer chez le tenant `creer-vm` confirmait son succes en attendant une reponse SSH. Le runner du SITE materialise le terrain de TOUS les tenants, mais la frontiere lui refuse d'entrer chez eux — c'est le sens meme de leur isolation. La premiere VM qu'il a creee a donc ete declaree en echec apres 600 secondes alors qu'elle tournait, avec l'adresse exacte que le plan lui destinait : Attente de SSH sur ops-01 ............ ECHEC: injoignable apres 600s. LA TENTATION ETAIT D'OUVRIR LE SSH du runner vers tous les tenants. Ca aurait repare la mesure en detruisant ce qu'elle protege : une machine capable d'entrer chez chaque locataire est precisement ce que cette architecture refuse d'avoir. L'agent invite repond sans rien ouvrir — l'API des hyperviseurs est deja le flux par lequel la VM vient d'etre creee, donc qui peut la creer peut la voir naitre — et il PROUVE DAVANTAGE. « Quelque chose ecoute sur le port 22 » ne dit ni quel systeme a demarre, ni si cloud-init a pose la bonne adresse. Sur ops-01 : 10.17.19.41 portee sur asgard — Debian GNU/Linux 13 (trixie) 6.12.101 Ses echecs distinguent deux causes tres differentes : « la machine demarre mais rapporte une AUTRE adresse — cloud-init, ou le pont sur lequel elle est posee », et « introuvable sur la fabric ». ATTENDRE LA DISPONIBILITE A CHANGE DE MAIN. Guetter cloud-init et la liberation de dpkg appartient a qui va CONFIGURER : `deployer` le fait desormais, la ou il se contentait d'un `ping` unique. Il y gagne ce qu'il n'avait pas — attendre le verrou APT, faute de quoi la premiere couche echouait dessus. Contrepartie assumee : un nom d'hote errone patiente au lieu d'echouer vite ; echouer vite interdirait de chainer creation et deploiement, le geste central d'une reconstruction. P52 garde le couplage ferme. Deux controles negatifs verifies. make prouver : CONFORME, 52 OK, 0 echec. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 22:48:49 -04:00
@# `creer-vm` CONFIRME LA MATERIALISATION, PAS LA JOIGNABILITE (2026-08-27). Il
@# attendait une reponse SSH — un geste que le runner du SITE ne peut pas faire : la
@# frontiere lui refuse d'entrer chez les tenants, et c'est le sens meme de leur
@# isolation. La premiere VM creee par le runner a donc ete declaree en echec apres
@# 600 secondes alors qu'elle tournait, avec la bonne adresse.
@#
@# L'agent invite repond a cette question sans rien ouvrir — l'API des hyperviseurs
@# est deja le flux par lequel la VM vient d'etre creee — et il PROUVE DAVANTAGE :
@# il rapporte le systeme, le noyau et l'adresse effectivement portee, la ou SSH ne
@# disait que « quelque chose ecoute sur le port 22 ».
@#
@# ATTENDRE LA DISPONIBILITE est un autre geste, deplace chez `deployer` : cloud-init
@# et dpkg se guettent juste avant de configurer, par celui qui va configurer.
@if [[ "$(ATTENDRE)" != "false" ]]; then \
creer-vm : prouver la materialisation sans entrer chez le tenant `creer-vm` confirmait son succes en attendant une reponse SSH. Le runner du SITE materialise le terrain de TOUS les tenants, mais la frontiere lui refuse d'entrer chez eux — c'est le sens meme de leur isolation. La premiere VM qu'il a creee a donc ete declaree en echec apres 600 secondes alors qu'elle tournait, avec l'adresse exacte que le plan lui destinait : Attente de SSH sur ops-01 ............ ECHEC: injoignable apres 600s. LA TENTATION ETAIT D'OUVRIR LE SSH du runner vers tous les tenants. Ca aurait repare la mesure en detruisant ce qu'elle protege : une machine capable d'entrer chez chaque locataire est precisement ce que cette architecture refuse d'avoir. L'agent invite repond sans rien ouvrir — l'API des hyperviseurs est deja le flux par lequel la VM vient d'etre creee, donc qui peut la creer peut la voir naitre — et il PROUVE DAVANTAGE. « Quelque chose ecoute sur le port 22 » ne dit ni quel systeme a demarre, ni si cloud-init a pose la bonne adresse. Sur ops-01 : 10.17.19.41 portee sur asgard — Debian GNU/Linux 13 (trixie) 6.12.101 Ses echecs distinguent deux causes tres differentes : « la machine demarre mais rapporte une AUTRE adresse — cloud-init, ou le pont sur lequel elle est posee », et « introuvable sur la fabric ». ATTENDRE LA DISPONIBILITE A CHANGE DE MAIN. Guetter cloud-init et la liberation de dpkg appartient a qui va CONFIGURER : `deployer` le fait desormais, la ou il se contentait d'un `ping` unique. Il y gagne ce qu'il n'avait pas — attendre le verrou APT, faute de quoi la premiere couche echouait dessus. Contrepartie assumee : un nom d'hote errone patiente au lieu d'echouer vite ; echouer vite interdirait de chainer creation et deploiement, le geste central d'une reconstruction. P52 garde le couplage ferme. Deux controles negatifs verifies. make prouver : CONFORME, 52 OK, 0 echec. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 22:48:49 -04:00
params="$$(python3 scripts/inventory_host.py --inventaire $(INVENTAIRE_PRODUCTION) parametres-proxmox --hote $(HOTE))"; \
eval "$$params"; \
python3 scripts/attendre_materialisation.py \
--vmid "$$SETOPS_VMID" --ip "$$SETOPS_IP" --hote "$(HOTE)" \
--delai $(ATTENTE_HOTE); \
fi
inventaire-verifier: ansible-runtime _instance-requise ## Verifie que l'inventaire se parse (voute dechiffree)
ansible-inventory -i $(INVENTAIRE_LAB) --list > /dev/null
ansible-inventory -i $(INVENTAIRE_PRODUCTION) --list > /dev/null
python3 scripts/inventory_host.py --inventaire $(INVENTAIRE_PRODUCTION) verifier-playbooks --dossier-playbooks $(DOSSIER_PLAYBOOKS_GROUPES)
python3 scripts/inventory_host.py --inventaire $(INVENTAIRE_PRODUCTION) --dependances $(FICHIER_DEPENDANCES) verifier-dependances --dossier-playbooks $(DOSSIER_PLAYBOOKS_GROUPES)
python3 scripts/verifier_gui.py
python3 scripts/serveurs.py verifier
python3 scripts/applications.py verifier
python3 scripts/bases_donnees.py verifier
python3 scripts/domaines.py verifier
.PHONY: bases bases-verifier domaines domaines-verifier applications applications-verifier applications-bootstrap serveurs serveurs-verifier serveurs-bootstrap
serveurs: ## Liste les serveurs declares au plan
python3 scripts/serveurs.py lister
serveurs-verifier: ## Valide le registre des serveurs
python3 scripts/serveurs.py verifier
documentation : la tournee des 74 documents, parce qu un balayage ne lit pas La revision a commence par un balayage par motifs — chemins morts, cibles make absentes, comptes derives. Il a trouve une trentaine d ecarts et rate presque tout le reste : un motif ne voit que ce qui s exprime en motif. make hote-planifier en est l exemple. La cible EXISTE, donc le controle passait au vert. C est une cible depreciee qui refuse et sort en 2, recommandee par AGENTS.md, et qui contredit la REGLE D OR du meme fichier trois ecrans plus haut. Il fallait lire pour la voir. 74 documents lus un par un. 66 corriges, 8 exacts. CE QUI ETAIT FRANCHEMENT FAUX AGENTS.md, la source d autorite, annoncait la flotte pas encore executee contre des VM reelles. Elle a ete rasee et remontee depuis zero trois fois. ecosysteme-chezlepro.md, le document montre a un client, portait la meme phrase : il se sous-vendait gravement. courriel-conception.md s ouvrait sur aucun role n est encore ecrit, au-dessus de son propre paragraphe 1 qui les nomme. autorisation.md se terminait sur rien n est construit alors qu il rapporte des mesures datees du role en fonctionnement. hebergeur-exploitation.md disait rien n est fait d un depot qui existe. filiation-emancipation.md se contredisait a deux ecrans de distance. DES MODELES DECRITS D APRES UN MONDE ANTERIEUR Le resolveur : cinq documents decrivaient un Unbound par VM en opt-in, trois le donnaient en exemple d integration FACULTATIVE — il est universel depuis le 2026-08-24. L adressage de nomenclature-vm.md : reseau unique, VLAN 11-15, VMID a cinq chiffres. Le nommage SDN de sdn-evpn.md contre le code : c est le wiki qui avait raison. CE QUI CASSE AU PREMIER ESSAI Le nom du gabarit dore etait faux a quatre endroits, dont la procedure qui le FABRIQUE et le critere R2 de l epreuve d operateur independant. preparer-un-site-hebergeur.md avertissait qu une VM faite a la main serait detruite : raser derive du plan, il ne la detruira jamais — le risque est l inverse. Un mot de passe d essai en clair dans un depot public. DEUX PREUVES ETENDUES, ET UNE QUI SE TROMPAIT ELLE-MEME P57 couvre les groupes : elle a signale aussitot 29 groupes annonces au-dessus d un tableau qui en cite 40. P29 confronte le tableau de authentification.md aux declarations reelles : 12 annonces, 21 reels. Et P57 imposait un chiffre faux — 56 preuves alors que le depot en porte 57, la conditionnelle vivant hors de tout comptage. Un garde-fou qui fait respecter une erreur ajoute l assurance a l erreur. CE QUI RESTE, ET QU AUCUNE PREUVE NE TIENT Deux comptes trouves a la main. Et une lacune reelle : rien ne garde les meta/acces.yml — ni qu un service web-sso en porte un, ni que le groupe qu il nomme existe. P29 tient les positions d authentification, personne ne tient les habilitations. make prouver : CONFORME, 56 OK, 0 echec, 1 saute. 0 lien mort. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-06 16:18:23 -04:00
serveurs-bootstrap: ## REPRISE : (re)constitue plan/serveurs.yml depuis un inventaire existant
python3 scripts/serveurs.py bootstrap
.PHONY: instancier instancier-appliquer
instancier: _instance-requise ## Genere hosts.yml depuis le plan (sans l'appliquer)
python3 scripts/instancier.py generer
python3 scripts/instancier.py comparer
instancier-appliquer: _instance-requise ## Applique l'inventaire genere — FORCE=1 pour passer outre le diff
python3 scripts/instancier.py appliquer $(if $(FORCE),--force)
bases: ## Liste les bases de donnees declarees au plan
python3 scripts/bases_donnees.py lister
bases-verifier: ## Valide le registre des bases de donnees
python3 scripts/bases_donnees.py verifier
domaines: ## Liste les domaines declares au plan
python3 scripts/domaines.py lister
domaines-verifier: ## Valide le registre des domaines
python3 scripts/domaines.py verifier
applications: ## Liste les applications declarees au plan
python3 scripts/applications.py lister
applications-verifier: ## Valide le registre des applications
python3 scripts/applications.py verifier
applications-bootstrap: ## Amorce les applications declarees au plan
python3 scripts/applications.py bootstrap
inventaire-lister: ansible-runtime ## Affiche l'inventaire complet (JSON)
ansible-inventory -i $(FICHIER_INVENTAIRE) --list
inventaire-graphe: ansible-runtime ## Affiche le graphe des groupes de l'inventaire
ansible-inventory -i $(FICHIER_INVENTAIRE) --graph
inventaire-hote: ansible-runtime ## Affiche les variables derivees d'un hote — HOTE=<nom>
@if [[ -z "$(HOTE)" ]]; then \
printf '%s\n' 'Refus: relancer avec HOTE=nom_hote.'; \
exit 2; \
fi
ansible-inventory -i $(FICHIER_INVENTAIRE) --host $(HOTE)
inventaire-lab: ## Affiche le graphe de l'inventaire de laboratoire
$(MAKE) inventaire-graphe FICHIER_INVENTAIRE="$(INVENTAIRE_LAB)"
inventaire-production: ## Affiche le graphe de l'inventaire de production
$(MAKE) inventaire-graphe FICHIER_INVENTAIRE="$(INVENTAIRE_PRODUCTION)"
.PHONY: _verifier-acces-modele _verifier-privileges-modele preparer-modele verifier-modele nettoyer-modele
_verifier-acces-modele: ansible-runtime _modele-requis
ansible -i $(INVENTAIRE_MODELE) $(CIBLE_MODELE) $(UTILISATEUR_MODELE) -m ping -e ansible_become=false
_verifier-privileges-modele: ansible-runtime _modele-requis
ansible -i $(INVENTAIRE_MODELE) $(CIBLE_MODELE) $(UTILISATEUR_MODELE) -b -m command -a "whoami"
# Le gabarit dore n'est PAS un hote du tenant : sa configuration Proxmox le place sur le
# reseau de fabrication (192.168.12.x), pas dans le supernet. `instancier` emet donc
# `modeles_vm` toujours VIDE, et les etats d'un serveur ne connaissent que `actif` et
# `planifie` — rien ne pouvait y entrer. Resultat mesure le 2026-08-09 : trois cibles
# documentees qui ne pouvaient rien faire, et Ansible qui repondait « skipping: no hosts
# matched » sans que ce soit une erreur.
#
# On DESIGNE donc la machine explicitement : `MODELE_HOTE=<ip-ou-nom>`. A defaut, on
# refuse au lieu de ne rien faire — une commande qui ne fait rien en silence est pire
# qu'une commande absente.
, := ,
_modele-requis:
@if [[ -z "$(MODELE_HOTE)" ]] && ! ansible-inventory -i $(INVENTAIRE_LAB) --list 2>/dev/null | python3 -c 'import json,sys; d=json.load(sys.stdin); sys.exit(0 if (d.get("modeles_vm") or {}).get("hosts") else 1)' 2>/dev/null; then \
printf '%s\n' 'Refus: aucune VM de gabarit designee.'; \
printf '%s\n' ' Le gabarit vit sur le reseau de fabrication, pas dans le tenant :'; \
printf '%s\n' ' il ne peut pas venir du plan. Le designer explicitement —'; \
printf '%s\n' ' make preparer-modele MODELE_HOTE=192.168.12.99'; \
printf '%s\n' ' make verifier-modele MODELE_HOTE=192.168.12.99'; \
printf '%s\n' ' make nettoyer-modele MODELE_HOTE=192.168.12.99 CONFIRMER=true'; \
printf '%s\n' ' (la VM doit etre DEMARREE : un template Proxmox ne boote pas ;'; \
printf '%s\n' ' le convertir en VM ou en cloner une copie de travail.)'; \
exit 2; \
fi
# `-i "<hote>,"` : la virgule finale fait de la chaine un inventaire d'un seul hote.
INVENTAIRE_MODELE = $(if $(MODELE_HOTE),"$(MODELE_HOTE)$(,)",$(INVENTAIRE_LAB))
# Cible : la machine designee, ou le groupe si l'inventaire en contient un.
CIBLE_MODELE = $(if $(MODELE_HOTE),$(MODELE_HOTE),$(GROUPE_MODELE))
# Un inventaire d'UN SEUL HOTE (`-i "<ip>,"`) ne porte aucun `group_vars` : il faut donc
# lui donner l'utilisateur de connexion, sinon Ansible tente le compte local de
# l'operateur. Defaut `ansible`, qui est le `ciuser` du gabarit.
MODELE_UTILISATEUR ?= ansible
UTILISATEUR_MODELE = $(if $(MODELE_HOTE),-e ansible_user=$(MODELE_UTILISATEUR) -e cible_modele=$(MODELE_HOTE),)
preparer-modele: ansible-runtime _modele-requis _verifier-acces-modele _verifier-privileges-modele ## Prepare le gabarit dore (VM de reference clonee pour chaque hote)
ansible-playbook -i $(INVENTAIRE_MODELE) $(UTILISATEUR_MODELE) $(PLAYBOOK_PREPARER_MODELE)
verifier-modele: ansible-runtime _modele-requis ## Verifie le gabarit dore
ansible-playbook -i $(INVENTAIRE_MODELE) $(UTILISATEUR_MODELE) $(PLAYBOOK_VERIFIER_MODELE)
nettoyer-modele: ansible-runtime _modele-requis ## Nettoie le gabarit avant capture — exige CONFIRMER=true
@if [[ "$(CONFIRMER)" != "true" ]]; then \
printf '%s\n' 'Refus: relancer avec CONFIRMER=true pour le nettoyage final du modele.'; \
exit 2; \
fi
ansible-playbook -i $(INVENTAIRE_MODELE) $(UTILISATEUR_MODELE) $(PLAYBOOK_NETTOYER_MODELE) -e template_cleanup_confirm=true
.PHONY: _verifier-acces-hote _verifier-privileges-hote _attendre-hote faits verifier-hote
# Deux ATTENTES ACTIVES, sans lesquelles on ne peut pas enchainer creation et
# deploiement — donc sans lesquelles `make myDay` ne peut pas reconstruire seul.
#
# 1. SSH. `make creer-vm` rend la main des que Proxmox a DEMARRE la VM, pas quand elle
# repond. Verifier l'acces aussitot echoue presque toujours sur une machine neuve —
# et le symptome trompe : a travers la frontiere, le TCP s'etablit (SYN proxy) et
# l'echec se lit « Connection timed out during banner exchange ».
#
# 2. Le verrou dpkg. L'image Debian lance ses propres mises a jour au premier
# demarrage et tient `/var/lib/dpkg/lock-frontend` plusieurs minutes. Le premier
# `apt` d'Ansible echoue alors sur un verrou, pas sur une vraie erreur.
#
# `unattended-upgrades.service` est volontairement ABSENT de la condition : c'est un
# DEMON (Type=simple), toujours `active`. L'y inclure rendait l'attente impossible a
# satisfaire — elle echouait au bout du delai, systematiquement. Seules `apt-daily*`
# sont des one-shot, et c'est le VERROU qui dit si dpkg est reellement occupe.
#
# Les deux sont des COURSES de premier demarrage, pas des defauts de conception. On les
# attend au lieu de les subir. ATTENTE_HOTE (secondes) borne chacune : depasser le
# delai reste un echec, pour ne pas transformer une panne en attente infinie.
ATTENTE_HOTE ?= 600
_attendre-hote: ansible-runtime
@set -e; \
if [[ -z "$(LIMITE)" ]]; then printf '%s\n' 'Refus: LIMITE requis.'; exit 2; fi; \
fin=$$(( SECONDS + $(ATTENTE_HOTE) )); \
printf '%s' "Attente de SSH sur $(LIMITE) "; \
until ansible -i $(INVENTAIRE_PRODUCTION) $(LIMITE) -m ping -e ansible_become=false >/dev/null 2>&1; do \
if (( SECONDS > fin )); then printf '%s\n' " ECHEC: injoignable apres $(ATTENTE_HOTE)s."; exit 4; fi; \
printf '.'; sleep 5; \
done; \
printf '%s\n' " ok"; \
printf '%s' "Attente de cloud-init et des maj automatiques "; \
until ansible -i $(INVENTAIRE_PRODUCTION) $(LIMITE) -b -m shell -a \
'cloud-init status --wait >/dev/null 2>&1; \
! systemctl is-active --quiet apt-daily.service apt-daily-upgrade.service \
&& ! fuser /var/lib/dpkg/lock-frontend >/dev/null 2>&1' >/dev/null 2>&1; do \
if (( SECONDS > fin )); then printf '%s\n' " ECHEC: dpkg toujours occupe apres $(ATTENTE_HOTE)s."; exit 4; fi; \
printf '.'; sleep 10; \
done; \
printf '%s\n' " libere"
_verifier-acces-hote: ansible-runtime
ansible -i $(INVENTAIRE_PRODUCTION) $(LIMITE) -m ping -e ansible_become=false
_verifier-privileges-hote: ansible-runtime
ansible -i $(INVENTAIRE_PRODUCTION) $(LIMITE) -b -m command -a "whoami"
faits: ansible-runtime ## Interroge les faits Ansible de la flotte — LIMITE=<motif>
ansible -i $(INVENTAIRE_PRODUCTION) $(LIMITE) -m setup -a "filter=ansible_distribution*"
verifier-hote: ansible-runtime ## Passe le playbook de verification sur un hote
ansible-playbook -i $(INVENTAIRE_PRODUCTION) $(PLAYBOOK_VERIFIER_HOTE) $(OPTIONS_PLAYBOOK)