2026-06-24 20:17:46 -04:00
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
2026-06-30 16:08:25 -04:00
# 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)
2026-06-30 16:08:25 -04:00
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)
2026-06-24 20:17:46 -04:00
FICHIER_DEPENDANCES ?= docs/dependances-groupes.yml
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
VMID_MODELE ?=
VMID ?=
NOEUD_PROXMOX ?=
STOCKAGE_PROXMOX ?=
FORMAT_DISQUE ?=
TAILLE_DISQUE ?=
DISQUE_PROXMOX ?=
CIDR ?= 24
PASSERELLE ?=
DNS ?=
DHCP ?= false
CIUSER ?=
CLE_SSH_PUBLIQUE ?=
PONT_PROXMOX ?=
VLAN ?=
DEMARRER ?=
CLONE_COMPLET ?=
CONFIRMER ?= false
VERIFICATION ?= false
DIFF ?= false
ETIQUETTES ?=
SAUTER_ETIQUETTES ?=
VARIABLES ?=
OPTIONS_PLAYBOOK :=
i f n e q ( $( LIMITE ) , )
OPTIONS_PLAYBOOK += --limit $( LIMITE)
e n d i f
i f e q ( $( VERIFICATION ) , t r u e )
OPTIONS_PLAYBOOK += --check
e n d i f
i f e q ( $( DIFF ) , t r u e )
OPTIONS_PLAYBOOK += --diff
e n d i f
i f n e q ( $( ETIQUETTES ) , )
OPTIONS_PLAYBOOK += --tags $( ETIQUETTES)
e n d i f
i f n e q ( $( SAUTER_ETIQUETTES ) , )
OPTIONS_PLAYBOOK += --skip-tags $( SAUTER_ETIQUETTES)
e n d i f
i f n e q ( $( VARIABLES ) , )
OPTIONS_PLAYBOOK += -e $( VARIABLES)
e n d i f
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
2026-08-08 13:21:23 -04:00
ansible-runtime : ## Prepare le repertoire temporaire local d'Ansible (prerequis interne des cibles qui deploient)
2026-06-24 20:17:46 -04:00
@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
2026-08-08 13:21:23 -04:00
aide : ## Affiche l'aide detaillee du moteur (au-dela de cette liste)
2026-06-24 20:17:46 -04:00
@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'
2026-06-30 12:19:14 -04:00
@printf '%s\n' ' Configurer Proxmox et le Vault API (parametres: docs/config-proxmox.md):'
2026-06-24 20:17:46 -04:00
@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' ''
2026-07-07 03:08:09 -04:00
@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' ''
2026-06-24 20:17:46 -04:00
@printf '%s\n' 'Inventaires'
@printf '%s\n' ' Graphe de production:'
@printf '%s\n' ' make inventaire'
@printf '%s\n' ' Graphe explicite:'
@printf '%s\n' ' make inventaire-graphe FICHIER_INVENTAIRE=$(SETOPS_INSTANCE)/inventories/production/hosts.yml'
@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'
@printf '%s\n' ' FICHIER_INVENTAIRE=$(SETOPS_INSTANCE)/inventories/production/hosts.yml FICHIER_DEPENDANCES=docs/dependances-groupes.yml CONFIRMER=true'
.PHONY : lint
2026-08-08 13:21:23 -04:00
lint : ansible -runtime ## Passe ansible-lint sur tout le depot
2026-06-24 20:17:46 -04:00
ansible-lint
.PHONY : syntaxe syntaxe -modele syntaxe -nettoyage syntaxe -verification -modele syntaxe -verification -hote syntaxe -groupes syntaxe -proxmox
2026-08-08 13:21:23 -04:00
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)
2026-06-24 20:17:46 -04:00
2026-08-08 13:21:23 -04:00
syntaxe-modele : ansible -runtime ## Verifie la syntaxe du playbook de preparation du gabarit dore
2026-08-09 11:29:51 -04:00
ansible-playbook -i $( INVENTAIRE_MODELE) $( UTILISATEUR_MODELE) $( PLAYBOOK_PREPARER_MODELE) --syntax-check
2026-06-24 20:17:46 -04:00
2026-08-08 13:21:23 -04:00
syntaxe-verification-modele : ansible -runtime ## Verifie la syntaxe du playbook de verification du gabarit
2026-08-09 11:29:51 -04:00
ansible-playbook -i $( INVENTAIRE_MODELE) $( UTILISATEUR_MODELE) $( PLAYBOOK_VERIFIER_MODELE) --syntax-check
2026-06-24 20:17:46 -04:00
2026-08-08 13:21:23 -04:00
syntaxe-nettoyage : ansible -runtime ## Verifie la syntaxe du playbook de nettoyage du gabarit
2026-08-09 10:40:49 -04:00
ansible-playbook -i $( INVENTAIRE_MODELE) $( PLAYBOOK_NETTOYER_MODELE) --syntax-check
2026-06-24 20:17:46 -04:00
2026-08-08 13:21:23 -04:00
syntaxe-verification-hote : ansible -runtime ## Verifie la syntaxe du playbook de verification d'hote
2026-06-24 20:17:46 -04:00
ansible-playbook -i $( INVENTAIRE_PRODUCTION) $( PLAYBOOK_VERIFIER_HOTE) --syntax-check
2026-08-08 13:21:23 -04:00
syntaxe-groupes : ansible -runtime ## Verifie la syntaxe des 30 playbooks de groupe
2026-06-24 20:17:46 -04:00
@for playbook in $( DOSSIER_PLAYBOOKS_GROUPES) /*.yml; do \
ansible-playbook -i $( INVENTAIRE_PRODUCTION) " $$ playbook " --syntax-check; \
done
2026-08-08 13:21:23 -04:00
syntaxe-proxmox : ansible -runtime ## Verifie la syntaxe du playbook de clonage de VM
2026-06-24 20:17:46 -04:00
ansible-playbook -i localhost, $( PLAYBOOK_PROXMOX_CLONER_VM) --syntax-check
.PHONY : test
2026-08-08 13:21:23 -04:00
test : ## Lance les tests unitaires (derivation de nomenclature et d'inventaire)
2026-06-24 20:17:46 -04:00
python3 scripts/tests/test_inventory_host.py
2026-08-08 14:22:04 -04:00
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
2026-06-24 20:17:46 -04:00
.PHONY : verifier
2026-08-08 13:21:23 -04:00
verifier : lint test inventaire -verifier site -verifier flux -verifier syntaxe ## Rejoue les preuves SANS reecrire le rapport (verification rapide)
2026-07-20 21:03:20 -04:00
python3 scripts/prouver.py --verifier
2026-06-24 20:17:46 -04:00
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.
genome : les quatre depots sans lesquels un ecosysteme ne renait pas
Un ecosysteme Set-OPS ne se reproduit pas depuis ses machines, mais depuis QUATRE depots :
le moteur, le plan du tenant, le depot de l'hebergeur (sa fabric) et les modeles. Perdre
les VM coute du temps ; perdre ces quatre-la coute l'ecosysteme. Rien ne les nommait.
`scripts/genome.py` les DERIVE au lieu de les declarer : le moteur est ce depot,
l'instance vient de SETOPS_INSTANCE, l'hebergeur se lit du symlink underlay.yml, et les
modeles se reconnaissent a leur FORME — des plans en sous-dossiers, aucun a la racine.
Deux criteres appris d'un faux positif : sans le second, le detecteur designait le lab,
qui porte un lien `OPS-Technolibre -> ../OPS-Technolibre` que le motif traversait. Un
lien vers un frere n'est pas un contenu.
Trois cibles : `make genome` (constater), `genome-inscrire` (ecrire la parente),
`genome-verifier` (tient-elle encore ?).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 12:46:42 -04:00
# 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
Mise en conformité prouvable : registre d'affirmations + make prouver
Le dépôt fait / explique / prouve ce qu'il affirme, vérifiable en une commande.
- Phase 1 : docs/audit/affirmations.md — 54 affirmations publiques tracées vers
une commande de preuve et un statut (✅/🟡/❌/⚪).
- Phase 2 : CLAUDE.md réduit à un pointeur mince ; contradiction SSH levée (le
code applique déjà PasswordAuthentication no + AuthenticationMethods publickey,
conforme à AGENTS.md) ; section AGENTS « Codex » → « agents IA ».
- Phase 3 : parcours démarrage réparé (QUICKSTART renvoyait à un modèle absent,
chemins de voûte faux, commandes make périmées) ; make verifier vert
(ansible-lint 33 → 0 : site.yml généré nommé, pipefail, name[template]) ;
voûte Proxmox unifiée lue par le clonage (all/vault.yml).
- Phase 4 : make prouver → docs/audit/preuve-<date>.md, harnais rejouable qui
rappelle l'outillage existant (aucune validation réimplémentée).
- Phase 5 : parcours QUICKSTART prouvé hors-ligne sur le socle ; modèle socle
rendu valide (autorite interne → auto-heberge) ; split-brain d'inventaire
corrigé (repli sur le répertoire existant, pas principal/).
make prouver : 15 OK, 0 échec, 1 sautée (voûte). ansible-lint : 0 failure.
Écarts découverts en cours de traitement (AFF-097..100) : tous résolus.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 19:53:18 -04:00
# Harnais de preuve : rejoue les preuves automatisables du registre et ecrit
# docs/audit/preuve-<date>.md (piece justificative horodatee, rejouable).
2026-07-20 21:03:20 -04:00
# `make verifier` l'appelle en mode --verifier (preuves seules, aucun rapport ecrit).
Mise en conformité prouvable : registre d'affirmations + make prouver
Le dépôt fait / explique / prouve ce qu'il affirme, vérifiable en une commande.
- Phase 1 : docs/audit/affirmations.md — 54 affirmations publiques tracées vers
une commande de preuve et un statut (✅/🟡/❌/⚪).
- Phase 2 : CLAUDE.md réduit à un pointeur mince ; contradiction SSH levée (le
code applique déjà PasswordAuthentication no + AuthenticationMethods publickey,
conforme à AGENTS.md) ; section AGENTS « Codex » → « agents IA ».
- Phase 3 : parcours démarrage réparé (QUICKSTART renvoyait à un modèle absent,
chemins de voûte faux, commandes make périmées) ; make verifier vert
(ansible-lint 33 → 0 : site.yml généré nommé, pipefail, name[template]) ;
voûte Proxmox unifiée lue par le clonage (all/vault.yml).
- Phase 4 : make prouver → docs/audit/preuve-<date>.md, harnais rejouable qui
rappelle l'outillage existant (aucune validation réimplémentée).
- Phase 5 : parcours QUICKSTART prouvé hors-ligne sur le socle ; modèle socle
rendu valide (autorite interne → auto-heberge) ; split-brain d'inventaire
corrigé (repli sur le répertoire existant, pas principal/).
make prouver : 15 OK, 0 échec, 1 sautée (voûte). ansible-lint : 0 failure.
Écarts découverts en cours de traitement (AFF-097..100) : tous résolus.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 19:53:18 -04:00
.PHONY : prouver
2026-08-08 13:21:23 -04:00
prouver : ansible -runtime _instance -requise ## Execute les preuves et ecrit docs/audit/preuve-<date>.md
Mise en conformité prouvable : registre d'affirmations + make prouver
Le dépôt fait / explique / prouve ce qu'il affirme, vérifiable en une commande.
- Phase 1 : docs/audit/affirmations.md — 54 affirmations publiques tracées vers
une commande de preuve et un statut (✅/🟡/❌/⚪).
- Phase 2 : CLAUDE.md réduit à un pointeur mince ; contradiction SSH levée (le
code applique déjà PasswordAuthentication no + AuthenticationMethods publickey,
conforme à AGENTS.md) ; section AGENTS « Codex » → « agents IA ».
- Phase 3 : parcours démarrage réparé (QUICKSTART renvoyait à un modèle absent,
chemins de voûte faux, commandes make périmées) ; make verifier vert
(ansible-lint 33 → 0 : site.yml généré nommé, pipefail, name[template]) ;
voûte Proxmox unifiée lue par le clonage (all/vault.yml).
- Phase 4 : make prouver → docs/audit/preuve-<date>.md, harnais rejouable qui
rappelle l'outillage existant (aucune validation réimplémentée).
- Phase 5 : parcours QUICKSTART prouvé hors-ligne sur le socle ; modèle socle
rendu valide (autorite interne → auto-heberge) ; split-brain d'inventaire
corrigé (repli sur le répertoire existant, pas principal/).
make prouver : 15 OK, 0 échec, 1 sautée (voûte). ansible-lint : 0 failure.
Écarts découverts en cours de traitement (AFF-097..100) : tous résolus.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 19:53:18 -04:00
python3 scripts/prouver.py
2026-06-30 16:31:42 -04:00
.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
2026-08-08 13:21:23 -04:00
inventaire : inventaire -production ## Verifie l'inventaire et en affiche le graphe (lab puis production)
2026-06-24 20:17:46 -04:00
2026-06-30 16:31:42 -04:00
# 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
2026-08-08 13:21:23 -04:00
instance-utiliser : ## Bascule l'instance active (symlink instance/) vers un dossier frere — NOM=<dossier>
2026-06-30 16:31:42 -04:00
@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) "
2026-08-08 13:21:23 -04:00
instance-courante : ## Affiche vers quel ecosysteme pointe l'instance active
2026-06-30 16:31:42 -04:00
@printf 'instance -> %s\n' " $$ (readlink instance 2>/dev/null || echo '(non monté)') "
2026-07-23 11:26:01 -04:00
# 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.
2026-08-08 13:21:23 -04:00
instances : ## Liste les ecosystemes decouverts (dossiers freres) et signale les collisions d'index
2026-07-23 11:26:01 -04:00
@python3 scripts/instances.py
2026-07-23 15:49:16 -04:00
# (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.
2026-08-08 13:21:23 -04:00
plan-recette : ## Regenere docs/audit/plan-de-recette.md depuis le wiki
2026-07-23 15:49:16 -04:00
@python3 scripts/plan_recette.py
2026-07-23 14:13:36 -04:00
# Liste les modèles disponibles (socle + SETOPS_MODELES) pour créer une instance.
2026-08-08 13:21:23 -04:00
instance-modeles : ## Liste les modeles d'ecosysteme disponibles
2026-07-23 14:13:36 -04:00
@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
2026-08-08 13:21:23 -04:00
instance-creer : ## Cree un nouvel ecosysteme depuis un modele — NOM=<nom> MODELE=<modele>
2026-07-23 14:13:36 -04:00
@python3 scripts/instance_creer.py --nom " $( NOM) " --modele " $( MODELE) " \
$( if $( INDEX) ,--index $( INDEX) ,)
Créer un modèle (make model-creer) — base + promotion d'instance
Symétrique d'instance-creer, produit un modèle dans le dépôt PRIVÉ
(Set-OPS-Modeles/, jamais exemples/modeles/). scripts/model_creer.py, 2 modes :
- MODE=base : copie un modèle générique.
- MODE=instance : PROMEUT une instance en modèle (généralise identité ->
exemple.*, index -> 1, production -> false, nftables_admin_ssh -> [], cle de
sauvegarde videe, proxmox.yml generique [D1], inventaire principal [D2]).
Surete : aucun secret ne sort (vault.yml/proxmox.vault.yml jamais copies ; refus
si l'un subsiste ; .example conserve). Copie CIBLEE (plan/ + inventories/ seulement,
symlinks resolus) -> robuste aux depots imbriques et boucles de symlinks. Le modele
produit doit valider (modeles.py verifier), sinon annule.
Teste en isolement (depot prive jamais touche) : promotion du labo reussie +
validee (aucun chezlepro residuel, aucun secret, proxmox generique) ; copie socle
OK ; refus d'une instance INVALIDE (garde-fou : hote fantome detecte).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 14:43:06 -04:00
# 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
2026-08-08 13:21:23 -04:00
model-creer : ## Cree un modele d'ecosysteme — MODE=<mode> NOM=<nom>
Créer un modèle (make model-creer) — base + promotion d'instance
Symétrique d'instance-creer, produit un modèle dans le dépôt PRIVÉ
(Set-OPS-Modeles/, jamais exemples/modeles/). scripts/model_creer.py, 2 modes :
- MODE=base : copie un modèle générique.
- MODE=instance : PROMEUT une instance en modèle (généralise identité ->
exemple.*, index -> 1, production -> false, nftables_admin_ssh -> [], cle de
sauvegarde videe, proxmox.yml generique [D1], inventaire principal [D2]).
Surete : aucun secret ne sort (vault.yml/proxmox.vault.yml jamais copies ; refus
si l'un subsiste ; .example conserve). Copie CIBLEE (plan/ + inventories/ seulement,
symlinks resolus) -> robuste aux depots imbriques et boucles de symlinks. Le modele
produit doit valider (modeles.py verifier), sinon annule.
Teste en isolement (depot prive jamais touche) : promotion du labo reussie +
validee (aucun chezlepro residuel, aucun secret, proxmox generique) ; copie socle
OK ; refus d'une instance INVALIDE (garde-fou : hote fantome detecte).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 14:43:06 -04:00
@python3 scripts/model_creer.py --mode " $( MODE) " --nom " $( NOM) " \
$( if $( BASE) ,--base $( BASE) ,) $( if $( SOURCE) ,--source $( SOURCE) ,) $( if $( DEST) ,--dest $( DEST) ,)
2026-08-08 13:21:23 -04:00
config : ## Affiche la configuration Proxmox lue par le moteur
2026-06-24 20:17:46 -04:00
python3 scripts/config_proxmox.py
2026-08-08 13:21:23 -04:00
inventaire-ui : _instance -requise ## Ouvre la console d'exploitation (GUI web) sur l'inventaire actif
2026-06-24 20:17:46 -04:00
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
2026-08-08 13:21:23 -04:00
hote-afficher : ansible -runtime ## Affiche tout ce que le plan derive pour un hote — HOTE=<nom>
2026-06-24 20:17:46 -04:00
@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)
2026-08-08 13:21:23 -04:00
appliquer : ansible -runtime ## Applique un groupe a la flotte — GROUPE=<groupe>
2026-06-24 20:17:46 -04:00
@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)'
2026-08-08 13:21:23 -04:00
deployer : _instance -requise ## Deploie un hote, couche par couche, dans l'ordre du graphe — HOTE=<nom>
2026-06-24 20:17:46 -04:00
@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 ; \
2026-08-24 19:14:08 -04:00
$( VAULT_UNE_FOIS) \
2026-06-24 20:17:46 -04:00
$( MAKE) _verifier-acces-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) "
2026-07-07 03:08:09 -04:00
.PHONY : site site -verifier deployer -tout
2026-08-08 13:21:23 -04:00
site : ansible -runtime ## Regenere playbooks/site.yml depuis les couches et le graphe de dependances
2026-07-07 03:08:09 -04:00
python3 scripts/orchestrer.py ecrire
ansible-playbook -i $( INVENTAIRE_PRODUCTION) playbooks/site.yml --syntax-check
2026-08-08 13:21:23 -04:00
site-verifier : ## Verifie que playbooks/site.yml correspond aux couches declarees
2026-07-07 03:08:09 -04:00
python3 scripts/orchestrer.py verifier
.PHONY : flux flux -verifier
2026-08-08 13:21:23 -04:00
flux : ansible -runtime ## Regenere le registre des flux et les regles nftables depuis les meta/flux.yml
2026-07-07 03:08:09 -04:00
python3 scripts/resoudre_flux.py registre
python3 scripts/resoudre_flux.py nftables
2026-07-07 15:50:18 -04:00
.PHONY : devis -reseau
2026-07-23 17:15:27 -04:00
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) ,)
2026-07-07 15:50:18 -04:00
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,)
proxmox-fw : un applicateur, et un refus assume
Reconcilie les trois couches du devis : 26 IPSets, 36 groupes, affectations aux
VM. Cree, met a jour, retire — meme contrat que les applicateurs de la frontiere
et du SDN.
Il n'active JAMAIS le pare-feu du datacenter : ce reglage vaut pour toutes les
VM du cluster, y compris les 37 heritees sans regle, et le basculer couperait le
parc. L'ecart est signale a chaque execution ; la decision reste humaine.
Les 28 VM du devis n'existent pas encore : listees comme differees, pas comme
erreurs. Les objets poses sont donc inertes, ce qui rend l'application sure.
`scripts/proxmox_api.py` extrait ce que les deux applicateurs Proxmox partagent,
en particulier la recomposition du jeton dont la voute ne porte que le nom.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 19:39:33 -04:00
.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 : un applicateur qui cree, met a jour et RETIRE
Deuxieme applicateur du depot, meme contrat que celui de la frontiere. Deux
cibles : les objets de cluster par l'API Proxmox, la sortie du VRF par SSH —
`/etc/frr/frr.conf.local` n'est expose par aucune API.
Perimetre strict : seules les zones du devis et les anciens nommages listes sont
touches ; une zone inconnue est signalee et laissee intacte. Un VNet encore
branche a une VM est refuse, avec le nom des machines. L'ordre suit les
dependances : sous-reseaux, VNets, zones.
`devis_sdn.strophe_frr()` est la source unique : le devis l'affiche,
l'applicateur la compare au fichier distant.
Defaut trouve a la premiere execution : la lecture sans `sudo` echouait, un
`|| true` masquait l'echec, et un fichier present etait declare absent puis
reecrit. Trois etats distingues desormais : absent, illisible, different.
Rejeu a vide, six VRF avec leur defaut, sortie tenant fonctionnelle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 19:30:13 -04:00
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
2026-08-07 17:26:35 -04:00
.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
2026-08-08 07:33:49 -04:00
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
devis : sixieme instrument — « ce qui n'est pas declare est-il refuse ? »
devis_expositions pose la question positive (une exposition declaree
repond-elle). Celle-ci manquait, et ce n'est pas la meme : un pare-feu peut
servir tout ce qu'on lui demande ET laisser passer tout le reste.
Rien n'y est saisi. Les cibles sont les ports REELLEMENT en ecoute (sonder un
port ferme ne prouverait rien du pare-feu) ; la politique attendue est lue
dans devis_opnsense.py, la source qui configure la frontiere.
scripts/sonde_tcp.py refuse de conclure : un CONTROLE (adresse ou personne
n'ecoute) avant tout verdict — s'il repond, le releve est declare NUL. Et
etablir n'est pas livrer : banniere, sinon requete HTTP, sinon poignee TLS ;
sans reponse le verdict est AMBIGU, jamais « ouvert ».
Deux pieges rencontres en le construisant, corriges et commentes sur place :
la sonde « tenant » s'executait sur le poste (dans un play connection: local,
delegate_to herite de cette connexion) ; et 2 s de lecture faisaient ressortir
un Postfix sain en AMBIGU (postscreen retarde sa banniere expres) — porte a
8 s, compense par du parallelisme.
Premier verdict, frontiere en l'etat : 38 ecarts, dont collab-01:9980 qui
livre vraiment un HTTP 200 depuis le poste. Releve sortant declare NUL.
Verifie : ansible-lint Passed, prouver.py 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 21:03:28 -04:00
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
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
2026-08-09 09:46:17 -04:00
ports-verifier : ## Deux roles co-localises revendiquent-ils le meme port ? (lecture seule)
@python3 scripts/verifier_ports.py
P32 / D-72 : tout intrant exige par un role est fourni par l'instance
Premier des trois chantiers rendus evidents par la reconstruction. Il aurait
trouve son defaut n1 — amorcage_acces_courriel — SANS RIEN DETRUIRE.
Un assert de role declare un contrat ; rien ne verifiait que l'instance
l'honore, et le manque ne se voit qu'au moment ou la garde s'execute — donc,
pour un intrant d'amorcage, seulement en repartant de rien.
Satisfait par : defaut non vide (vault_* compris, gardes par P18), set_fact de
resolveur, ou declaration de l'inventaire. Aucune voute dechiffree : la preuve
reste statique.
Deux fois mon instrument a accuse le composant a sa place, avant meme sa
premiere execution utile : il criait au manque sur
serveur_postfix_mailstore_hote, pourtant fourni — je ne lisais pas le fichier
d'inventaire, puis je n'y cherchais que les blocs vars: alors qu'instancier
ecrit sous le nom d'hote.
Verifie dans les deux sens : 30 exigences satisfaites sur le reel ; sur un
double sans la declaration d'hier, le defaut n1 est nomme, code 1.
Harnais : 32 preuves, 0 echec, 0 sautee.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 09:40:25 -04:00
intrants-verifier : ## Tout intrant qu'un role EXIGE est-il fourni par l'instance ? (lecture seule)
@python3 scripts/verifier_intrants.py
2026-08-08 06:55:06 -04:00
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
2026-08-06 14:51:51 -04:00
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
pare-feu Proxmox : le filtrage est-ouest intra-tenant, dérivé (P25)
`make devis-proxmox-fw` : 34 groupes de sécurité, 40 règles, 2 tenants —
depuis les 57 flux intra-tenant que le registre connaissait déjà.
Défense en profondeur, pas remplacement : l'hyperviseur filtre puis l'hôte
destinataire filtre à nouveau. Coût de maintenance nul, les deux barrières
lisent le registre par les MÊMES fonctions — la duplication est dans
l'application, jamais dans la décision.
Un IPSet par rôle porte les membres, les groupes y renvoient : ajouter un
hôte à un rôle met à jour toutes les règles qui l'autorisent, en un endroit.
Garde ajoutée après coup : Proxmox limite un nom de groupe à 18 caractères.
Ma première version tronquait sans vérifier — deux rôles tronqués au même nom
auraient fusionné leurs règles, donnant à une VM les autorisations d'un rôle
qu'elle ne porte pas, silencieusement. Le préfixe porte maintenant l'index
plutôt que l'étiquette, et une garde échoue sur toute collision. Exercée.
Conséquence consignée : tout ce qui entre dans un tenant passant par la
frontière, le contrôleur Ansible aussi — l'OPNsense devient un prérequis de
déploiement, pas une étape parmi d'autres.
Preuves : 25 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 11:43:32 -04:00
.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,)
2026-08-08 13:21:23 -04:00
devis-proxmox-fw-verifier : ## Verifie le devis du pare-feu est-ouest Proxmox (aucune ecriture)
pare-feu Proxmox : le filtrage est-ouest intra-tenant, dérivé (P25)
`make devis-proxmox-fw` : 34 groupes de sécurité, 40 règles, 2 tenants —
depuis les 57 flux intra-tenant que le registre connaissait déjà.
Défense en profondeur, pas remplacement : l'hyperviseur filtre puis l'hôte
destinataire filtre à nouveau. Coût de maintenance nul, les deux barrières
lisent le registre par les MÊMES fonctions — la duplication est dans
l'application, jamais dans la décision.
Un IPSet par rôle porte les membres, les groupes y renvoient : ajouter un
hôte à un rôle met à jour toutes les règles qui l'autorisent, en un endroit.
Garde ajoutée après coup : Proxmox limite un nom de groupe à 18 caractères.
Ma première version tronquait sans vérifier — deux rôles tronqués au même nom
auraient fusionné leurs règles, donnant à une VM les autorisations d'un rôle
qu'elle ne porte pas, silencieusement. Le préfixe porte maintenant l'index
plutôt que l'étiquette, et une garde échoue sur toute collision. Exercée.
Conséquence consignée : tout ce qui entre dans un tenant passant par la
frontière, le contrôleur Ansible aussi — l'OPNsense devient un prérequis de
déploiement, pas une étape parmi d'autres.
Preuves : 25 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 11:43:32 -04:00
python3 scripts/devis_proxmox_fw.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,)
2026-08-08 13:21:23 -04:00
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,)
2026-08-08 13:21:23 -04:00
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
2026-08-08 13:21:23 -04:00
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
2026-07-24 14:56:38 -04:00
.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
2026-08-08 13:21:23 -04:00
flux-verifier : ## Verifie que le registre des flux correspond aux meta/flux.yml des roles
2026-07-07 03:08:09 -04:00
python3 scripts/resoudre_flux.py verifier
2026-07-07 03:19:51 -04:00
.PHONY : valider
2026-08-08 13:21:23 -04:00
valider : ansible -runtime ## Passe la recette de validation sur la flotte
2026-07-07 03:19:51 -04:00
ansible-playbook -i $( INVENTAIRE_PRODUCTION) playbooks/valider.yml
2026-07-07 03:08:09 -04:00
.PHONY : wiki -publier
2026-08-08 13:21:23 -04:00
wiki-publier : ## Publie le wiki (wiki/) vers la forge
2026-07-07 03:08:09 -04:00
@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 ; \
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 ; \
wiki : intègre les 8 vues annotées à « Le GUI (console d'exploitation) »
La série d'illustrations (commit précédent) rejoint sa page. En fin de section
② de l'unité, une galerie « Les vues, annotées » présente les 8 figures — vues
éditables (Serveurs, Applications, Bases × 2, Domaines) puis dérivées (Flux,
Couches, Réseau) — chacune un SVG auto-contenu avec ses repères + légende.
Mécanique, sans duplication dans l'arbre source :
- symlink wiki/img -> ../docs/img : foyer unique dans docs/img/, refs relatives
(img/*.svg) qui résolvent en local et sur le navigateur de dépôt ;
- make wiki-publier embarque désormais les docs/img/*-annote.svg (déréférencés)
dans le wiki Forgejo publié — jusqu'ici seules les .md voyageaient, donc aucune
image n'aurait rendu une fois publiée.
Preuves : 22 OK / 0 échec / 1 sauté (dont P22 « plan de recette à jour »).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 13:57:39 -04:00
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 ; \
2026-07-07 03:08:09 -04:00
cd " $$ tmp/wiki " ; \
if [ [ -z " $$ (git status --porcelain) " ] ] ; then \
printf '%s\n' 'Wiki deja a jour (aucun changement).' ; \
exit 0; \
fi ; \
git add -A; \
sha = " $$ (git -C " $( CURDIR) " rev-parse --short HEAD 2>/dev/null || echo inconnu)" ; \
git commit --quiet -m " Publication du wiki depuis le depot (source: $$ sha) " ; \
git push --quiet; \
printf '%s\n' 'Wiki publie.'
2026-08-08 13:21:23 -04:00
deployer-tout : _instance -requise ## Deploie TOUTE la flotte dans l'ordre des couches
2026-07-07 03:08:09 -04:00
@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) " ; \
if [ [ -n " $$ vault_chiffre " && -z " $$ {ANSIBLE_VAULT_PASSWORD_FILE:-} " ] ] ; 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.
2026-08-24 19:14:08 -04:00
# --- 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.
d e f i n e V A U L T _ U N E _ F O I S
vault_chiffre = " $$ (grep -rlsIF ' $$ ANSIBLE_VAULT' $( dir $( INVENTAIRE_PRODUCTION) ) group_vars 2>/dev/null | head -1 || true) " ; \
if [ [ -n " $$ vault_chiffre " && -z " $$ {ANSIBLE_VAULT_PASSWORD_FILE:-} " ] ] ; 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 ;
e n d e f
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
2026-07-07 03:08:09 -04:00
@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; \
2026-08-24 19:14:08 -04:00
$( 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; \
2026-07-07 03:08:09 -04:00
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 ; \
2026-07-07 03:08:09 -04:00
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 ; \
2026-07-07 03:08:09 -04:00
printf '\nToutes les VM actives sont creees.\n'
2026-08-24 23:11:32 -04:00
# --- 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.
2026-08-24 23:11:32 -04:00
@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 ; \
2026-08-25 09:34:02 -04:00
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)
2026-08-24 23:11:32 -04:00
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.
.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)
2026-08-24 23:11:32 -04:00
.PHONY : site -creer
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) ; \
2026-08-24 23:11:32 -04:00
params = " $$ (python3 scripts/site_machines.py parametres-proxmox --machine " $$ m")" ; \
eval " $$ params " ; \
$( MAKE) cloner-vm \
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 " \
NOEUD_PROXMOX = " $$ SETOPS_NOEUD " \
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
VMID_MODELE = " $$ SETOPS_VMID_MODELE " \
CLONE_COMPLET = " $$ SETOPS_CLONE_COMPLET " \
2026-08-24 23:11:32 -04:00
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 )) " ; \
2026-08-24 23:11:32 -04:00
done ; \
printf '\nLes machines du site sont creees.\n'
2026-07-07 03:08:09 -04:00
.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
2026-08-08 14:22:04 -04:00
raser : ansible -runtime _instance -requise ## DESTRUCTIF : detruit les VM derivees du plan — exige CONFIRMER=true ET INSTANCE=<nom>
2026-08-09 11:50:34 -04:00
@python3 scripts/raser.py $( if $( INSTANCE) ,--instance $( INSTANCE) ) $( if $( HOTE) ,--hote $( HOTE) ) $( if $( filter true,$( CONFIRMER) ) ,--confirmer)
2026-08-08 14:22:04 -04:00
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.
2026-08-08 13:21:23 -04:00
reconstruire : _instance -requise ## Reconstruit un ecosysteme depuis zero : VM puis deploiement complet
2026-07-07 03:08:09 -04:00
@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; \
2026-07-07 03:08:09 -04:00
$( MAKE) flotte-creer CONFIRMER = true; \
$( MAKE) _attendre-flotte; \
2026-08-08 15:29:57 -04:00
$( MAKE) _amorcer-socle; \
2026-07-07 03:08:09 -04:00
$( 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
2026-08-08 15:29:57 -04:00
# « 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'
2026-08-08 14:34:00 -04:00
myDay : reconstruire ## Alias strict de `reconstruire` (meme cible, memes gardes)
2026-07-07 03:08:09 -04:00
2026-08-08 13:21:23 -04:00
deployer-groupe : ## Deploie un seul groupe sur toute la flotte — GROUPE=<groupe>
2026-06-24 20:17:46 -04:00
@if [ [ -z " $( GROUPE) " ] ] ; then \
printf '%s\n' 'Refus: relancer avec GROUPE=nom_groupe.' ; \
exit 2; \
fi
$( MAKE) appliquer GROUPE = " $( GROUPE) "
.PHONY : verifier -deploiement
2026-08-08 13:21:23 -04:00
verifier-deploiement : ansible -runtime ## Verifie l'etat de la flotte apres deploiement
2026-06-24 20:17:46 -04:00
@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 ; \
2026-07-07 03:08:09 -04:00
vault_chiffre = " $$ (grep -rlsIF ' $$ ANSIBLE_VAULT' $( dir $( INVENTAIRE_PRODUCTION) ) group_vars 2>/dev/null | head -1 || true) " ; \
2026-06-24 20:17:46 -04:00
if [ [ -n " $$ vault_chiffre " && -z " $$ {ANSIBLE_VAULT_PASSWORD_FILE:-} " ] ] ; 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
2026-08-08 13:21:23 -04:00
cloner-vm : ansible -runtime ## Clone une VM depuis le gabarit dore — HOTE=<nom> VMID=<id>
2026-06-24 20:17:46 -04:00
@if [ [ -z " $( HOTE) " || -z " $( VMID) " ] ] ; then \
printf '%s\n' 'Refus: relancer avec HOTE=nom VMID=id_clone.' ; \
exit 2; \
fi
premiere VM tenant : quatre defauts leves sur le chemin
`infra-pki-01` recreee pour eprouver la chaine complete. Tout ce qui derive du
seed est exact : VMID, pont t17serv sans etiquette, adresse, passerelle, pool,
et desormais le gabarit de calcul.
1. Le pare-feu est-ouest aurait enferme Ansible : `t<idx>-srv-debian`
n'autorisait SSH que depuis la flotte du tenant, et `admin_de(nom)` etait
collecte puis jamais utilise. IPSet `t<idx>-admin` dedie + regle sourcee
dessus. Pas d'ajout a `flotte`, qui sert aussi LDAP, SQL et les metriques.
2. L'applicateur ne convergeait pas : Proxmox range `x/32` en `x`, et la
comparaison litterale laissait un ecart perpetuel. `_norm()` le ferme.
3. `cloner-vm` refusait toute VM de tenant : son garde exigeait un VLAN, vide
par construction en SDN. Accepte desormais si un pont est fourni.
4. Le clonage ignore `cores`/`memory` : la VM heritait du gabarit (2/2048) au
lieu du plan (1/1024). Une tache les repose apres le clone.
Et le troisieme verrou de D-64 : `proxmox_clone_parefeu_interface: true`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 20:33:12 -04:00
@# 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.' ; \
2026-06-24 20:17:46 -04:00
exit 2; \
fi
premiere VM tenant : quatre defauts leves sur le chemin
`infra-pki-01` recreee pour eprouver la chaine complete. Tout ce qui derive du
seed est exact : VMID, pont t17serv sans etiquette, adresse, passerelle, pool,
et desormais le gabarit de calcul.
1. Le pare-feu est-ouest aurait enferme Ansible : `t<idx>-srv-debian`
n'autorisait SSH que depuis la flotte du tenant, et `admin_de(nom)` etait
collecte puis jamais utilise. IPSet `t<idx>-admin` dedie + regle sourcee
dessus. Pas d'ajout a `flotte`, qui sert aussi LDAP, SQL et les metriques.
2. L'applicateur ne convergeait pas : Proxmox range `x/32` en `x`, et la
comparaison litterale laissait un ecart perpetuel. `_norm()` le ferme.
3. `cloner-vm` refusait toute VM de tenant : son garde exigeait un VLAN, vide
par construction en SDN. Accepte desormais si un pont est fourni.
4. Le clonage ignore `cores`/`memory` : la VM heritait du gabarit (2/2048) au
lieu du plan (1/1024). Une tache les repose apres le clone.
Et le troisieme verrou de D-64 : `proxmox_clone_parefeu_interface: true`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 20:33:12 -04:00
@if [ [ -n " $( VLAN) " ] ] && { ! [ [ " $( VLAN) " = ~ ^[ 0-9] +$$ ] ] || ( ( 10#$( VLAN) < 1 || 10#$( VLAN) > 4094 ) ) ; } ; then \
2026-06-24 20:17:46 -04:00
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 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
-e proxmox_clone_pool = " $$ (python3 scripts/devis_proxmox_pools.py --pool-actif) " \
2026-06-24 20:17:46 -04:00
) ; \
[ [ -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) " ) ; \
2026-06-30 10:07:04 -04:00
[ [ -n " $( COEURS) " ] ] && extra_vars += ( -e proxmox_clone_coeurs = " $( COEURS) " ) ; \
[ [ -n " $( MEMOIRE) " ] ] && extra_vars += ( -e proxmox_clone_memoire = " $( MEMOIRE) " ) ; \
2026-06-24 20:17:46 -04:00
[ [ -n " $( DISQUE_PROXMOX) " ] ] && extra_vars += ( -e proxmox_clone_disque = " $( DISQUE_PROXMOX) " ) ; \
[ [ -n " $( DNS) " ] ] && extra_vars += ( -e proxmox_clone_dns = " $( DNS) " ) ; \
gabarit : deriver le domaine de recherche, et vider resolv.conf a la capture
Question « on peut l'optimiser ? » — mesure avant de repondre. Rien a gagner
cote performance (UEFI/q35, virtio-scsi-single + iothread, discard+ssd,
x86-64-v2-AES, agent, balloon 0) ni cote paquets : le socle est deja cuit dans
l'image.
Ce que le gabarit transporte, c'est son lieu de naissance. searchdomain
chezlepro.ca — le domaine PUBLIC — etait herite par les 14 VM, faute de
proxmox_clone_domaines_recherche defini. Desormais derive de domaine_interne
via SETOPS_DOMAINE, par le mecanisme qui existait deja pour le DNS.
Honnetement : ca ne reparait pas de panne. serveur_debian reecrit resolv.conf
au deploiement sans ligne search — verifie sur la flotte. C'etait faux et ca
ne tenait que par chance.
template_cleanup vide desormais /etc/resolv.conf a la capture : un gabarit ne
transporte aucune identite de reseau.
Notes : mtu 9000 est un reglage MORT (les clones tournent en 1500) ; et le
groupe modeles_vm est VIDE, donc preparer/verifier/nettoyer-modele n'ont
aucune cible.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 10:25:03 -04:00
[ [ -n " $( DOMAINE_RECHERCHE) " ] ] && extra_vars += ( -e proxmox_clone_domaines_recherche = " $( DOMAINE_RECHERCHE) " ) ; \
2026-06-24 20:17:46 -04:00
[ [ -n " $( CIUSER) " ] ] && extra_vars += ( -e proxmox_clone_ciuser = " $( CIUSER) " ) ; \
[ [ -n " $( CLE_SSH_PUBLIQUE) " ] ] && extra_vars += ( -e proxmox_clone_cle_publique_fichier = " $( CLE_SSH_PUBLIQUE) " ) ; \
[ [ -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 = ( ) ; \
2026-07-01 15:38:42 -04:00
vault_file = "" ; \
for d in lab principal production; do \
Mise en conformité prouvable : registre d'affirmations + make prouver
Le dépôt fait / explique / prouve ce qu'il affirme, vérifiable en une commande.
- Phase 1 : docs/audit/affirmations.md — 54 affirmations publiques tracées vers
une commande de preuve et un statut (✅/🟡/❌/⚪).
- Phase 2 : CLAUDE.md réduit à un pointeur mince ; contradiction SSH levée (le
code applique déjà PasswordAuthentication no + AuthenticationMethods publickey,
conforme à AGENTS.md) ; section AGENTS « Codex » → « agents IA ».
- Phase 3 : parcours démarrage réparé (QUICKSTART renvoyait à un modèle absent,
chemins de voûte faux, commandes make périmées) ; make verifier vert
(ansible-lint 33 → 0 : site.yml généré nommé, pipefail, name[template]) ;
voûte Proxmox unifiée lue par le clonage (all/vault.yml).
- Phase 4 : make prouver → docs/audit/preuve-<date>.md, harnais rejouable qui
rappelle l'outillage existant (aucune validation réimplémentée).
- Phase 5 : parcours QUICKSTART prouvé hors-ligne sur le socle ; modèle socle
rendu valide (autorite interne → auto-heberge) ; split-brain d'inventaire
corrigé (repli sur le répertoire existant, pas principal/).
make prouver : 15 OK, 0 échec, 1 sautée (voûte). ansible-lint : 0 failure.
Écarts découverts en cours de traitement (AFF-097..100) : tous résolus.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 19:53:18 -04:00
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 ; \
2026-07-01 15:38:42 -04:00
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 ; \
2026-07-01 15:38:42 -04:00
if [ [ -n " $$ vault_file " && -f " $$ vault_file " ] ] ; then \
2026-06-24 20:17:46 -04:00
read -r premiere_ligne < " $$ vault_file " || true; \
case " $$ premiere_ligne " in \
'$$ANSIBLE_VAULT' *) \
if [ [ -z " $$ {ANSIBLE_VAULT_PASSWORD_FILE:-} " ] ] ; then \
vault_args += ( --ask-vault-pass ) ; \
fi ; \
; ; \
esac ; \
fi ; \
ansible-playbook -i localhost, $( PLAYBOOK_PROXMOX_CLONER_VM) " $$ {vault_args[@]} " " $$ {extra_vars[@]} "
2026-08-08 13:21:23 -04:00
creer-vm : _instance -requise ## Cree une VM et attend qu'elle soit joignable — HOTE=<nom>
2026-06-24 20:17:46 -04:00
@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 " ; \
$( 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) } " \
2026-06-24 20:17:46 -04:00
STOCKAGE_PROXMOX = " $$ {SETOPS_STOCKAGE:- $( STOCKAGE_PROXMOX) } " \
TAILLE_DISQUE = " $$ {SETOPS_DISQUE:- $( TAILLE_DISQUE) } " \
2026-06-30 10:07:04 -04:00
COEURS = " $$ {SETOPS_COEURS:- $( COEURS) } " \
MEMOIRE = " $$ {SETOPS_MEMOIRE:- $( MEMOIRE) } " \
2026-06-24 20:17:46 -04:00
NOEUD_PROXMOX = " $$ {SETOPS_NOEUD:- $( NOEUD_PROXMOX) } " \
VMID_MODELE = " $( VMID_MODELE) " \
FORMAT_DISQUE = " $( FORMAT_DISQUE) " \
DISQUE_PROXMOX = " $( DISQUE_PROXMOX) " \
dns : client_unbound universel, et un resolveur d'amorcage
Une VM ne peut pas s'installer sans resoudre des noms. PowerDNS repond
UNIQUEMENT pour la zone souveraine et ne recurse pour personne : il manquait un
resolveur recursif. `client_unbound` est l'outil ecrit pour ca — il rejoint
`client_journal` et `client_metrique` parmi les integrations universelles.
`infra-dns-01` est exempte (PowerDNS occupe son port 53), et
`serveur_powerdns_listen_addresses` passe de 0.0.0.0 a l'adresse de l'hote pour
laisser 127.0.0.1:53 libre. Mais l'appartenance au groupe est AUSSI ce qui ouvre
le port 53 a la frontiere : en exemptant la machine, je lui retirais le droit de
resoudre. `serveur_powerdns` declare donc son propre flux sortant — il ne
recurse pour personne, mais doit resoudre pour lui-meme.
Nouvel intrant `dns_amorcage`, derive jusqu'a `make creer-vm`. Cloud-init
l'ecrit bien mais sans effet : `dns-nameservers` exige `resolvconf`, absent du
gabarit, et installer resolvconf demande apt, qui demande la resolution.
`serveur_debian` pose donc le resolveur en pre_tasks, avant le premier apt, avec
une garde qui respecte la bascule ulterieure de client_unbound.
Defaut corrige en chemin : `_intrants_communs()` lisait `instance/` en dur ; le
chemin derive maintenant de l'inventaire recu, et un test le prouve.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 21:41:58 -04:00
DNS = " $$ {SETOPS_DNS:- $( DNS) } " \
gabarit : deriver le domaine de recherche, et vider resolv.conf a la capture
Question « on peut l'optimiser ? » — mesure avant de repondre. Rien a gagner
cote performance (UEFI/q35, virtio-scsi-single + iothread, discard+ssd,
x86-64-v2-AES, agent, balloon 0) ni cote paquets : le socle est deja cuit dans
l'image.
Ce que le gabarit transporte, c'est son lieu de naissance. searchdomain
chezlepro.ca — le domaine PUBLIC — etait herite par les 14 VM, faute de
proxmox_clone_domaines_recherche defini. Desormais derive de domaine_interne
via SETOPS_DOMAINE, par le mecanisme qui existait deja pour le DNS.
Honnetement : ca ne reparait pas de panne. serveur_debian reecrit resolv.conf
au deploiement sans ligne search — verifie sur la flotte. C'etait faux et ca
ne tenait que par chance.
template_cleanup vide desormais /etc/resolv.conf a la capture : un gabarit ne
transporte aucune identite de reseau.
Notes : mtu 9000 est un reglage MORT (les clones tournent en 1500) ; et le
groupe modeles_vm est VIDE, donc preparer/verifier/nettoyer-modele n'ont
aucune cible.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 10:25:03 -04:00
DOMAINE_RECHERCHE = " $$ {SETOPS_DOMAINE:- $( DOMAINE_RECHERCHE) } " \
2026-06-24 20:17:46 -04:00
DHCP = " $( DHCP) " \
CIUSER = " $( CIUSER) " \
CLE_SSH_PUBLIQUE = " $( CLE_SSH_PUBLIQUE) " \
DEMARRER = " $( DEMARRER) " \
CLONE_COMPLET = " $( CLONE_COMPLET) "
2026-08-07 09:30:48 -04:00
@# ` creer-vm` rend une VM PRETE, pas seulement demarree : sans cette attente,
@# enchainer ` creer-vm` puis ` deployer` echoue presque toujours sur une machine
@# neuve. C'est ce qui separe une suite de commandes d' une reconstruction.
@if [ [ " $( ATTENDRE) " != "false" ] ] ; then \
$( MAKE) --no-print-directory _attendre-hote LIMITE = " $( HOTE) " ; \
fi
2026-06-24 20:17:46 -04:00
2026-08-08 13:21:23 -04:00
inventaire-verifier : ansible -runtime _instance -requise ## Verifie que l'inventaire se parse (voute dechiffree)
2026-06-24 20:17:46 -04:00
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
2026-08-08 13:21:23 -04:00
serveurs : ## Liste les serveurs declares au plan
2026-06-24 20:17:46 -04:00
python3 scripts/serveurs.py lister
2026-08-08 13:21:23 -04:00
serveurs-verifier : ## Valide le registre des serveurs
2026-06-24 20:17:46 -04:00
python3 scripts/serveurs.py verifier
2026-08-08 13:21:23 -04:00
serveurs-bootstrap : ## Amorce l'acces SSH aux serveurs neufs
2026-06-24 20:17:46 -04:00
python3 scripts/serveurs.py bootstrap
.PHONY : instancier instancier -appliquer
2026-08-08 13:21:23 -04:00
instancier : _instance -requise ## Genere hosts.yml depuis le plan (sans l'appliquer)
2026-06-24 20:17:46 -04:00
python3 scripts/instancier.py generer
python3 scripts/instancier.py comparer
2026-08-08 13:21:23 -04:00
instancier-appliquer : _instance -requise ## Applique l'inventaire genere — FORCE=1 pour passer outre le diff
2026-06-25 09:29:08 -04:00
python3 scripts/instancier.py appliquer $( if $( FORCE) ,--force)
2026-06-24 20:17:46 -04:00
2026-08-08 13:21:23 -04:00
bases : ## Liste les bases de donnees declarees au plan
2026-06-24 20:17:46 -04:00
python3 scripts/bases_donnees.py lister
2026-08-08 13:21:23 -04:00
bases-verifier : ## Valide le registre des bases de donnees
2026-06-24 20:17:46 -04:00
python3 scripts/bases_donnees.py verifier
2026-08-08 13:21:23 -04:00
domaines : ## Liste les domaines declares au plan
2026-06-24 20:17:46 -04:00
python3 scripts/domaines.py lister
2026-08-08 13:21:23 -04:00
domaines-verifier : ## Valide le registre des domaines
2026-06-24 20:17:46 -04:00
python3 scripts/domaines.py verifier
2026-08-08 13:21:23 -04:00
applications : ## Liste les applications declarees au plan
2026-06-24 20:17:46 -04:00
python3 scripts/applications.py lister
2026-08-08 13:21:23 -04:00
applications-verifier : ## Valide le registre des applications
2026-06-24 20:17:46 -04:00
python3 scripts/applications.py verifier
2026-08-08 13:21:23 -04:00
applications-bootstrap : ## Amorce les applications declarees au plan
2026-06-24 20:17:46 -04:00
python3 scripts/applications.py bootstrap
2026-08-08 13:21:23 -04:00
inventaire-lister : ansible -runtime ## Affiche l'inventaire complet (JSON)
2026-06-24 20:17:46 -04:00
ansible-inventory -i $( FICHIER_INVENTAIRE) --list
2026-08-08 13:21:23 -04:00
inventaire-graphe : ansible -runtime ## Affiche le graphe des groupes de l'inventaire
2026-06-24 20:17:46 -04:00
ansible-inventory -i $( FICHIER_INVENTAIRE) --graph
2026-08-08 13:21:23 -04:00
inventaire-hote : ansible -runtime ## Affiche les variables derivees d'un hote — HOTE=<nom>
2026-06-24 20:17:46 -04:00
@if [ [ -z " $( HOTE) " ] ] ; then \
printf '%s\n' 'Refus: relancer avec HOTE=nom_hote.' ; \
exit 2; \
fi
ansible-inventory -i $( FICHIER_INVENTAIRE) --host $( HOTE)
2026-08-08 13:21:23 -04:00
inventaire-lab : ## Affiche le graphe de l'inventaire de laboratoire
2026-06-24 20:17:46 -04:00
$( MAKE) inventaire-graphe FICHIER_INVENTAIRE = " $( INVENTAIRE_LAB) "
2026-08-08 13:21:23 -04:00
inventaire-production : ## Affiche le graphe de l'inventaire de production
2026-06-24 20:17:46 -04:00
$( MAKE) inventaire-graphe FICHIER_INVENTAIRE = " $( INVENTAIRE_PRODUCTION) "
.PHONY : _verifier -acces -modele _verifier -privileges -modele preparer -modele verifier -modele nettoyer -modele
2026-08-09 10:40:49 -04:00
_verifier-acces-modele : ansible -runtime _modele -requis
2026-08-09 11:29:51 -04:00
ansible -i $( INVENTAIRE_MODELE) $( CIBLE_MODELE) $( UTILISATEUR_MODELE) -m ping -e ansible_become = false
2026-08-09 10:40:49 -04:00
_verifier-privileges-modele : ansible -runtime _modele -requis
2026-08-09 11:29:51 -04:00
ansible -i $( INVENTAIRE_MODELE) $( CIBLE_MODELE) $( UTILISATEUR_MODELE) -b -m command -a "whoami"
2026-08-09 10:40:49 -04:00
# 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
2026-06-24 20:17:46 -04:00
2026-08-09 10:40:49 -04:00
# `-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) )
2026-06-24 20:17:46 -04:00
2026-08-09 11:29:51 -04:00
# 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) ,)
2026-08-09 10:40:49 -04:00
preparer-modele : ansible -runtime _modele -requis _verifier -acces -modele _verifier -privileges -modele ## Prepare le gabarit dore (VM de reference clonee pour chaque hote)
2026-08-09 11:29:51 -04:00
ansible-playbook -i $( INVENTAIRE_MODELE) $( UTILISATEUR_MODELE) $( PLAYBOOK_PREPARER_MODELE)
2026-06-24 20:17:46 -04:00
2026-08-09 10:40:49 -04:00
verifier-modele : ansible -runtime _modele -requis ## Verifie le gabarit dore
2026-08-09 11:29:51 -04:00
ansible-playbook -i $( INVENTAIRE_MODELE) $( UTILISATEUR_MODELE) $( PLAYBOOK_VERIFIER_MODELE)
2026-06-24 20:17:46 -04:00
2026-08-09 10:40:49 -04:00
nettoyer-modele : ansible -runtime _modele -requis ## Nettoie le gabarit avant capture — exige CONFIRMER=true
2026-06-24 20:17:46 -04:00
@if [ [ " $( CONFIRMER) " != "true" ] ] ; then \
printf '%s\n' 'Refus: relancer avec CONFIRMER=true pour le nettoyage final du modele.' ; \
exit 2; \
fi
2026-08-09 11:29:51 -04:00
ansible-playbook -i $( INVENTAIRE_MODELE) $( UTILISATEUR_MODELE) $( PLAYBOOK_NETTOYER_MODELE) -e template_cleanup_confirm = true
2026-06-24 20:17:46 -04:00
2026-08-07 09:30:48 -04:00
.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"
2026-06-24 20:17:46 -04:00
_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"
2026-08-08 13:21:23 -04:00
faits : ansible -runtime ## Interroge les faits Ansible de la flotte — LIMITE=<motif>
2026-06-24 20:17:46 -04:00
ansible -i $( INVENTAIRE_PRODUCTION) $( LIMITE) -m setup -a "filter=ansible_distribution*"
2026-08-08 13:21:23 -04:00
verifier-hote : ansible -runtime ## Passe le playbook de verification sur un hote
2026-06-24 20:17:46 -04:00
ansible-playbook -i $( INVENTAIRE_PRODUCTION) $( PLAYBOOK_VERIFIER_HOTE) $( OPTIONS_PLAYBOOK)