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 # UNE VOUTE, UNE CLE — ET ANSIBLE LES TIENT TOUTES A LA FOIS (2026-08-28). # # Un seul mot de passe ouvrait les six voutes de la flotte, celle de la fabric comprise. # Depuis la separation, chaque ecosysteme a la sienne, sous # `~/.config/setops-vault-`. # # UN `ANSIBLE_VAULT_PASSWORD_FILE` NE PEUT PLUS SUFFIRE : `cloner_vm_debian.yml` charge # la voute du TENANT puis celle de l'UNDERLAY dans la meme execution — les identifiants # Proxmox appartiennent a l'hebergeur, le reste au locataire. `ANSIBLE_VAULT_IDENTITY_LIST` # porte plusieurs secrets et les essaie tous ; un seul export suffit donc pour les 28 # appels a `ansible-playbook`, sans en toucher un seul. # # `?=` : une valeur deja posee dans l'environnement gagne — la GUI et les runners peuvent # nommer leurs propres cles sans que ce fichier ait a les connaitre. export ANSIBLE_VAULT_IDENTITY_LIST ?= $(shell python3 scripts/voutes.py identites 2>/dev/null) # Inventaire de l'instance : un seul par instance dans le modèle « séparation par # instance ». Détection rétro-compatible : principal > production > lab. # Surchargeable : make … SETOPS_INVENTAIRE=chemin/hosts.yml # # LA PRÉCÉDENCE SUIT CELLE DU CODE (`instancier._inventaire`) : fichier existant, puis # RÉPERTOIRE existant, puis défaut. Le troisième niveau manquait, et comme cette variable # FORCE la cible, `make instancier` sur une instance neuve visait un `principal/` # inexistant alors que le plan vivait dans `production/` — l'échec du tout premier geste # du QUICKSTART, sur le modèle public (mesuré le 2026-08-19). Sans `make`, la même # commande réussissait : c'est la variable exportée qui décidait, pas le code. SETOPS_INVENTAIRE ?= $(firstword \ $(wildcard $(SETOPS_INSTANCE)/inventories/principal/hosts.yml) \ $(wildcard $(SETOPS_INSTANCE)/inventories/production/hosts.yml) \ $(patsubst %/,%/hosts.yml,$(wildcard $(SETOPS_INSTANCE)/inventories/principal/)) \ $(patsubst %/,%/hosts.yml,$(wildcard $(SETOPS_INSTANCE)/inventories/production/)) \ $(SETOPS_INSTANCE)/inventories/principal/hosts.yml) export SETOPS_INVENTAIRE # Inventaire « modèle » (construction du golden template) : lab > principal > production. INVENTAIRE_LAB ?= $(firstword $(wildcard $(SETOPS_INSTANCE)/inventories/lab/hosts.yml) $(wildcard $(SETOPS_INSTANCE)/inventories/principal/hosts.yml) $(SETOPS_INSTANCE)/inventories/production/hosts.yml) INVENTAIRE_PRODUCTION ?= $(SETOPS_INVENTAIRE) FICHIER_INVENTAIRE ?= $(SETOPS_INVENTAIRE) FICHIER_DEPENDANCES ?= docs/dependances-groupes.yml # LES COUCHES DE L'INSEMINATION — CE QUE LE SITE POSE CHEZ UN TENANT, ET RIEN DE PLUS. # # Un ecosysteme neuf ne s'amorce pas lui-meme : quelqu'un doit poser sa premiere machine # et lui donner de quoi continuer. Le runner du SITE le fait — socle, moteur, plan, # plancher de resolution — puis s'arrete. # # CES DEUX-LA ET AUCUNE AUTRE, parce qu'elles ne demandent AUCUN SECRET. Le site ne # detient pas la voute d'un tenant, et ne doit jamais la detenir : ce qui vient apres # (`client_pki`, `serveur_ops_tenant`...) reclame des valeurs qu'il n'a pas. La liste # n'est donc pas une commodite, c'est la LIGNE DE PARTAGE DU POUVOIR, et la preuve P54 # refuse toute couche qui y ferait entrer un secret. COUCHES_INSEMINATION ?= serveur_debian serveur_ops GROUPE_MODELE ?= modeles_vm GROUPE_DEBIAN ?= serveur_debian GROUPE_HOTES_ACTIFS ?= hotes_actifs LIMITE ?= $(GROUPE_DEBIAN) HOTE ?= ADRESSE_IP ?= GROUPES ?= $(GROUPE_DEBIAN) GROUPE ?= $(GROUPE_DEBIAN) UTILISATEUR_ANSIBLE ?= ansible # LE GABARIT VIENT DU SITE, PAS DU TENANT (2026-09-01). # # « Les ressources du SITE font autorite pour tous ses artefacts. » Le gabarit en est un : # c'est de lui que descend chaque VM de chaque tenant. Son VMID vivait pourtant dans les # group_vars de l'ecosysteme — deux tenants pouvaient donc cloner deux gabarits differents # sans que rien ne le dise, et un tenant decidait d'un objet qui n'est pas a lui. # # Meme mouvement que l'INDEX, tranche le 2026-08-25 : le site ALLOUE, le tenant RECOIT. # # `?=` : une valeur donnee en ligne de commande gagne — necessaire pour eprouver un # gabarit neuf avant de le declarer. Et si le site n'en declare aucun, la valeur reste # vide et le plan du tenant reprend la main : degrader, jamais deviner. VMID_MODELE ?= $(shell python3 scripts/underlay.py --gabarit 2>/dev/null | sed -n 's/^vmid=//p') # NOTE SUR LE PIEGE QU'ON VIENT DE FERMER : `creer-vm` passait # `VMID_MODELE="$$SETOPS_VMID_MODELE"` a `cloner-vm`, or `inventory_host.py` n'emet PAS # cette variable — elle valait donc le VIDE, et ce vide ECRASAIT la valeur derivee. Le # gabarit du tenant reprenait la main sans que rien ne le dise. `:-` rend la derivation au # site quand l'inventaire ne dit rien. VMID ?= NOEUD_PROXMOX ?= # LE STOCKAGE DU CLONE SUIT CELUI DU GABARIT, ET IL EST DECLARE (2026-09-10). # # Vide, `qm clone` heritait SILENCIEUSEMENT du stockage de la source. Ca marchait, et # c'est bien la le probleme : quand le gabarit a change de stockage, la destination a # suivi sans qu'aucun fichier ne le dise. Le site le declare desormais # (`gabarit.stockage`), et cette ligne le rend a la commande de clonage. # # `SETOPS_STOCKAGE` — le `stockage:` d'une machine au plan du tenant — garde la priorite # (voir `creer-vm`) : un ecosysteme peut poser une machine ailleurs, ce qui n'est pas la # meme question que « d'ou nait la flotte ». STOCKAGE_PROXMOX ?= $(shell python3 scripts/underlay.py --gabarit 2>/dev/null | sed -n 's/^stockage=//p') FORMAT_DISQUE ?= TAILLE_DISQUE ?= DISQUE_PROXMOX ?= CIDR ?= 24 PASSERELLE ?= DNS ?= DHCP ?= false CIUSER ?= CLE_SSH_PUBLIQUE ?= # LA CLE D'AMORCAGE VOYAGE PAR L'ENVIRONNEMENT, ET ARRIVE EN JSON. MESURE (2026-08-28). # # Une cle publique contient des ESPACES — type, materiel, commentaire — et deux couches # les mangent en silence : # # `ansible-playbook -e cle=valeur` decoupe la chaine AU SHLEX : tout ce qui suit le # premier espace devient d'autres paires cle=valeur. Le module recevait # `"sshkeys": "ssh-ed25519"` — le premier mot, rien d'autre. # # Et une variable `make` passee a un sous-make est un chemin tout aussi fragile pour # une valeur a espaces. # # Proxmox repondait `500 SSH public key validation error` : un message qui ne parle ni # de shlex, ni de make, ni d'espaces. Le contrôle qui l'a isole : rejouer `cloner-vm` # SANS la cle — la tache passe. # # D'ou la forme retenue : l'environnement (`SETOPS_CLES_AMORCAGE`, exporte par # `creer-vm`) pour le transport, et `-e '{"...": "..."}'` en JSON pour l'entree dans # Ansible — le seul mode de `-e` qui ne decoupe rien. PONT_PROXMOX ?= VLAN ?= DEMARRER ?= CLONE_COMPLET ?= CONFIRMER ?= false VERIFICATION ?= false DIFF ?= false ETIQUETTES ?= SAUTER_ETIQUETTES ?= VARIABLES ?= OPTIONS_PLAYBOOK := ifneq ($(LIMITE),) OPTIONS_PLAYBOOK += --limit $(LIMITE) endif ifeq ($(VERIFICATION),true) OPTIONS_PLAYBOOK += --check endif ifeq ($(DIFF),true) OPTIONS_PLAYBOOK += --diff endif ifneq ($(ETIQUETTES),) OPTIONS_PLAYBOOK += --tags $(ETIQUETTES) endif ifneq ($(SAUTER_ETIQUETTES),) OPTIONS_PLAYBOOK += --skip-tags $(SAUTER_ETIQUETTES) endif ifneq ($(VARIABLES),) OPTIONS_PLAYBOOK += -e $(VARIABLES) endif PLAYBOOK_PREPARER_MODELE := playbooks/modeles_vm/debian13_proxmox_preparer.yml PLAYBOOK_VERIFIER_MODELE := playbooks/modeles_vm/debian13_proxmox_verifier.yml PLAYBOOK_NETTOYER_MODELE := playbooks/modeles_vm/debian13_proxmox_nettoyer.yml PLAYBOOK_VERIFIER_HOTE := playbooks/maintenance/verifier_hote_debian.yml PLAYBOOK_PROXMOX_CLONER_VM := playbooks/proxmox/cloner_vm_debian.yml DOSSIER_PLAYBOOKS_GROUPES := playbooks/groupes .DEFAULT_GOAL := aide .PHONY: ansible-runtime ansible-runtime: ## Prepare le repertoire temporaire local d'Ansible (prerequis interne des cibles qui deploient) @mkdir -p "$(ANSIBLE_LOCAL_TEMP)" @mkdir -p "$(ANSIBLE_SSH_CONTROL_PATH_DIR)" .PHONY: _instance-requise _instance-requise: @if [[ ! -f "$(SETOPS_INSTANCE)/plan/serveurs.yml" ]]; then \ printf '%s\n' "Aucune instance configuree : '$(SETOPS_INSTANCE)/plan' introuvable."; \ printf '%s\n' "Demarre avec QUICKSTART.md. En bref :"; \ printf '%s\n' " cp -r exemples/modeles/ ../mon-instance && ln -s ../mon-instance instance"; \ printf '%s\n' " modeles disponibles : $$(ls exemples/modeles 2>/dev/null | grep -v '\.md' | tr '\n' ' ')"; \ exit 2; \ fi .PHONY: aide aide: ## Affiche l'aide detaillee du moteur (au-dela de cette liste) @printf '%s\n' 'Set-OPS — moteur d ecosystemes numeriques souverains' @printf '%s\n' '' @printf '%s\n' 'Nouveau ? -> QUICKSTART.md (de zero a ton ecosysteme sur Proxmox)' @printf '%s\n' 'Flux: editer le plan -> make instancier -> make instancier-appliquer -> make deployer' @printf '%s\n' '' @printf '%s\n' 'VM' @printf '%s\n' ' Creer une VM (VMID/IP/VLAN/passerelle lus dans le plan):' @printf '%s\n' ' make creer-vm HOTE=web-frontal-01' @printf '%s\n' ' Cloner seulement, sans passer par le plan:' @printf '%s\n' ' make cloner-vm HOTE=web-frontal-01 VMID=95301 VLAN=15 ADRESSE_IP=10.0.2.31 PASSERELLE=10.0.2.1' @printf '%s\n' ' Configurer Proxmox et le Vault API (parametres: docs/config-proxmox.md):' @printf '%s\n' ' make config' @printf '%s\n' '' @printf '%s\n' 'Hotes' @printf '%s\n' ' Planifier/modifier un hote: editer le plan, puis regenerer:' @printf '%s\n' ' editer instance/plan/serveurs.yml (ou la vue Serveurs du GUI)' @printf '%s\n' ' make instancier-appliquer' @printf '%s\n' ' Verifier un deploiement a blanc (dry-run):' @printf '%s\n' ' make verifier-deploiement HOTE=web-frontal-01' @printf '%s\n' ' Remettre un hote en conformite selon ses groupes:' @printf '%s\n' ' make deployer HOTE=web-frontal-01' @printf '%s\n' ' Afficher un hote:' @printf '%s\n' ' make hote-afficher HOTE=web-frontal-01' @printf '%s\n' ' Diagnostiquer:' @printf '%s\n' ' make verifier-hote LIMITE=web-frontal-01' @printf '%s\n' '' @printf '%s\n' 'Groupes' @printf '%s\n' ' Appliquer un groupe complet:' @printf '%s\n' ' make deployer-groupe GROUPE=serveur_debian' @printf '%s\n' ' Convention:' @printf '%s\n' ' groupe serveur_debian -> playbooks/groupes/serveur_debian.yml' @printf '%s\n' '' @printf '%s\n' 'Ecosysteme complet (orchestrateur)' @printf '%s\n' ' Ordre de deploiement (couches + graphe):' @printf '%s\n' ' make site-verifier # valide la coherence couches/graphe' @printf '%s\n' ' python3 scripts/orchestrer.py ordre' @printf '%s\n' ' (Re)generer playbooks/site.yml ordonne:' @printf '%s\n' ' make site' @printf '%s\n' ' CONFIGURER la flotte existante, couche par couche (2b, VM deja creees):' @printf '%s\n' ' make deployer-tout CONFIRMER=true # (MODE_CHECK=1 pour un essai a blanc idempotent)' @printf '%s\n' ' CREER toutes les VM du plan (2a, clone Proxmox):' @printf '%s\n' ' make flotte-creer CONFIRMER=true' @printf '%s\n' ' RECONSTRUIRE from-zero = creer les VM PUIS deployer (2a+2b, VM inexistantes):' @printf '%s\n' ' make reconstruire CONFIRMER=true # alias: make myDay CONFIRMER=true' @printf '%s\n' '' @printf '%s\n' 'Flux reseau (pare-feu / audit)' @printf '%s\n' ' Matrice d audit + apercus nftables resolus (NON actives):' @printf '%s\n' ' make flux # -> docs/registre-flux.md + instance/flux-genere/*.nft' @printf '%s\n' ' make flux-verifier # valide schema + coherence de matrice' @printf '%s\n' '' @printf '%s\n' 'Remise au client — EN DEUX TEMPS (docs/remise-au-client.md)' @printf '%s\n' ' Temps 1, l IDENTITE : le client gouverne ses gens des le premier jour.' @printf '%s\n' ' make remise-recenser # ce qui partirait, sans rien ecrire' @printf '%s\n' ' make ca-racine ; make ca-empreinte # sa racine d AC, et le temoin a comparer' @printf '%s\n' ' make remise-paquet VERS=/media//REMISE' @printf '%s\n' ' make remise-inscrire RECU_PAR="Prenom Nom" COURRIEL="…"' @printf '%s\n' ' Temps 2, la MACHINE : sa cle entre, la notre sort, la voute est re-cletee.' @printf '%s\n' ' make remise-verifier # second temps du, ou echu ?' @printf '%s\n' ' make remise-recleer CONFIRMER=true # mesure la revocation, puis estampille' @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.//.wiki.git' @printf '%s\n' '' @printf '%s\n' 'Inventaires' @printf '%s\n' ' Graphe de production:' @printf '%s\n' ' make inventaire' @printf '%s\n' ' Graphe explicite:' @printf '%s\n' ' make inventaire-graphe FICHIER_INVENTAIRE=$(SETOPS_INVENTAIRE)' @printf '%s\n' ' Verifier les inventaires:' @printf '%s\n' ' make inventaire-verifier' @printf '%s\n' ' Lister les donnees brutes:' @printf '%s\n' ' make inventaire-lister' @printf '%s\n' ' Interface locale de gestion:' @printf '%s\n' ' make inventaire-ui' @printf '%s\n' '' @printf '%s\n' 'Modele Debian 13 Proxmox' @printf '%s\n' ' Construire et verifier:' @printf '%s\n' ' make preparer-modele' @printf '%s\n' ' make verifier-modele' @printf '%s\n' ' Nettoyage final protege:' @printf '%s\n' ' make nettoyer-modele CONFIRMER=true' @printf '%s\n' '' @printf '%s\n' 'Validation' @printf '%s\n' ' make syntaxe' @printf '%s\n' ' make lint' @printf '%s\n' ' make verifier' @printf '%s\n' '' @printf '%s\n' 'Variables frequentes' @printf '%s\n' ' HOTE=web-frontal-01 GROUPE=serveur_debian GROUPES="serveur_debian serveur_durci"' @printf '%s\n' ' VMID=95301 VLAN=15 ADRESSE_IP=10.0.2.31 PASSERELLE=10.0.2.1' @printf '%s\n' ' FICHIER_INVENTAIRE=$(SETOPS_INVENTAIRE) FICHIER_DEPENDANCES=docs/dependances-groupes.yml CONFIRMER=true' .PHONY: lint lint: ansible-runtime ## Passe ansible-lint sur tout le depot ansible-lint .PHONY: syntaxe syntaxe-modele syntaxe-nettoyage syntaxe-verification-modele syntaxe-verification-hote syntaxe-groupes syntaxe-proxmox syntaxe: syntaxe-modele syntaxe-verification-modele syntaxe-nettoyage syntaxe-verification-hote syntaxe-groupes syntaxe-proxmox ## Verifie la syntaxe de TOUS les playbooks (modele, hote, groupes, proxmox) syntaxe-modele: ansible-runtime ## Verifie la syntaxe du playbook de preparation du gabarit dore ansible-playbook -i $(INVENTAIRE_MODELE) $(UTILISATEUR_MODELE) $(PLAYBOOK_PREPARER_MODELE) --syntax-check syntaxe-verification-modele: ansible-runtime ## Verifie la syntaxe du playbook de verification du gabarit ansible-playbook -i $(INVENTAIRE_MODELE) $(UTILISATEUR_MODELE) $(PLAYBOOK_VERIFIER_MODELE) --syntax-check syntaxe-nettoyage: ansible-runtime ## Verifie la syntaxe du playbook de nettoyage du gabarit ansible-playbook -i $(INVENTAIRE_MODELE) $(PLAYBOOK_NETTOYER_MODELE) --syntax-check syntaxe-verification-hote: ansible-runtime ## Verifie la syntaxe du playbook de verification d'hote ansible-playbook -i $(INVENTAIRE_PRODUCTION) $(PLAYBOOK_VERIFIER_HOTE) --syntax-check syntaxe-groupes: ansible-runtime ## Verifie la syntaxe de TOUS les playbooks de groupe (playbooks/groupes/*.yml) @for playbook in $(DOSSIER_PLAYBOOKS_GROUPES)/*.yml; do \ ansible-playbook -i $(INVENTAIRE_PRODUCTION) "$$playbook" --syntax-check; \ done syntaxe-proxmox: ansible-runtime ## Verifie la syntaxe du playbook de clonage de VM ansible-playbook -i localhost, $(PLAYBOOK_PROXMOX_CLONER_VM) --syntax-check .PHONY: test test: ## Lance les tests unitaires (derivation de nomenclature et d'inventaire) python3 scripts/tests/test_inventory_host.py python3 scripts/tests/test_raser.py python3 scripts/tests/test_raser_resultat.py python3 scripts/tests/test_adressage_derive.py python3 scripts/tests/test_gui_intrants.py python3 scripts/tests/test_devis_placement.py python3 scripts/tests/test_sonder.py # LA SONDE QUI RAPPORTE CE QUI DISTINGUE (2026-08-28). Trois faux diagnostics en une # journee, tous dus a l'instrument : `curl` et `bash /dev/tcp` ecrasent « la machine # n'existe pas », « une politique refuse » et « la frontiere se tait » dans le meme mot. # L'INSEMINATION, CODIFIEE (2026-08-28). Elle a d'abord ete conduite A LA MAIN depuis le # runner du site — donc hors du depot, donc sans preuve, ce que ce projet refuse. # # `TENANT=` PLUTOT QUE LE SYMLINK `instance`. Le runner du SITE materialise et amorce # PLUSIEURS tenants ; pointer un lien global sur l'un d'eux le ferait se prendre pour ce # tenant. Il en NOMME un par commande, et `SETOPS_INSTANCE` suffit a le dire — verifie le # 2026-08-28. C'est aussi ce qui borne le couplage que `creer-vm` imposait en silence : # rien ne disait sur quels tenants ce lien avait le droit de pointer, ni jusqu'a quand. # `env -u SETOPS_INVENTAIRE` EST OBLIGATOIRE, ET UN GARDE-FOU L'A PROUVE. # # Le make parent EXPORTE SETOPS_INVENTAIRE, derive de l'instance montee. Sans le retirer, # la resolution du tenant demande heritait de l'inventaire d'un AUTRE ecosysteme : # # REFUS : SETOPS_INVENTAIRE designe un inventaire HORS de l'instance demandee # # Le refus vient d'inventory_rules, pas d'ici — il a intercepte une insemination qui # aurait vise le mauvais ecosysteme (2026-08-28). C'est exactement le genre d'erreur # qu'un runner de site, qui sert plusieurs locataires, commettrait en silence. .PHONY: inseminer inseminer: ansible-runtime ## Le SITE amorce le runner d'un tenant — TENANT= [HOTE=] @set -e; \ if [[ -z "$(TENANT)" ]]; then \ printf '%s\n' 'Refus: relancer avec TENANT=, ex. TENANT=OPS-Chezlepro.'; \ exit 2; \ fi; \ if [[ ! -d "../$(TENANT)/plan" ]]; then \ printf '%s\n' 'Refus: ../$(TENANT)/plan introuvable — ce n est pas un ecosysteme.'; \ exit 2; \ fi; \ inv="$$(env -u SETOPS_INVENTAIRE SETOPS_INSTANCE=../$(TENANT) python3 -c 'import sys; sys.path.insert(0,"scripts"); from inventory_rules import inventaire_de; print(inventaire_de() or "")')"; \ if [[ -z "$$inv" || ! -f "$$inv" ]]; then \ printf '%s\n' "Refus: aucun inventaire pour $(TENANT). Le generer chez lui (make instancier)."; \ exit 2; \ fi; \ hote="$(HOTE)"; \ if [[ -z "$$hote" ]]; then \ hote="$$(python3 -c 'import sys,yaml; d=yaml.safe_load(open(sys.argv[1])) or {}; p=[d.get("all",{})]; out=[]; \ [ (out.extend((n.get("children") or {}).get("serveur_ops_tenant",{}).get("hosts",{}) or {}), p.extend((n.get("children") or {}).values())) for n in iter(lambda: p.pop() if p else None, None) ]; print(" ".join(sorted(set(out))))' "$$inv")"; \ fi; \ if [[ -z "$$hote" ]]; then \ printf '%s\n' "Refus: $(TENANT) ne declare aucun hote portant serveur_ops_tenant."; \ printf '%s\n' "L insemination vise LE RUNNER d un tenant, jamais une machine ordinaire."; \ exit 2; \ fi; \ if (( $$(wc -w <<< "$$hote") > 1 )); then \ printf '%s\n' "Refus: plusieurs runners declares ($$hote). Nommer lequel avec HOTE=."; \ exit 2; \ fi; \ python3 scripts/verifier_cle_amorcage.py || exit 2; \ printf 'Insemination de %s chez %s — couches sans secret : %s\n\n' "$$hote" "$(TENANT)" "$(COUCHES_INSEMINATION)"; \ for groupe in $(COUCHES_INSEMINATION); do \ printf '=== %s ===\n' "$$groupe"; \ SETOPS_INSTANCE="../$(TENANT)" SETOPS_INVENTAIRE="$$inv" \ ansible-playbook -i "$$inv" "$(DOSSIER_PLAYBOOKS_GROUPES)/$$groupe.yml" --limit "$$hote"; \ done; \ printf '\n%s\n' "$$hote porte desormais le moteur, son plan et la carte de la fabric."; \ printf '%s\n' "IL N EST PAS ARME : ni voute, ni cle — le site ne les detient pas."; \ printf '%s\n' "L armer est le geste d un humain, depuis son poste :"; \ printf '%s\n' " make appliquer GROUPE=serveur_ops_tenant (voir docs/filiation-emancipation.md)" # PROUVER QU'UN LIEN DE FILIATION EST COUPE — la quatrieme ligne de # `docs/filiation-emancipation.md`, celle qu'on oublie : « une emancipation non prouvee # est une emancipation non faite ». # # GESTE DESTRUCTIF ET REVERSIBLE : il COUPE l'amont pendant quelques secondes pour voir # si la fonction tient sans lui. Sonder ne prouverait rien — tant que l'amont repond, une # fonction qui marche ne dit pas d'ou vient l'octet. # # LE MEME ESSAI REND LES DEUX VERDICTS, et c'est voulu : « emancipe, prouve » quand la # fonction tient, « pas emancipe, dependance REELLE » quand elle casse. Le second est le # controle negatif du premier. .PHONY: emancipation-prouver emancipation-prouver: ansible-runtime _instance-requise ## Prouve qu un lien est coupe — SERVICE= [HOTE=] CONFIRMER=true @set -e; \ if [[ -z "$(SERVICE)" ]]; then \ printf '%s\n' 'Refus: relancer avec SERVICE=.'; \ exit 2; \ fi; \ if [[ "$(CONFIRMER)" != "true" ]]; then \ printf '%s\n' 'Refus: cette preuve COUPE l amont quelques secondes pour mesurer.'; \ printf '%s\n' 'La coupure est retiree quoi qu il arrive (bloc `always`), mais'; \ printf '%s\n' 'pendant ce temps la fonction eprouvee peut echouer sur cet hote.'; \ printf '%s\n' 'Relancer avec CONFIRMER=true.'; \ exit 2; \ fi; \ ansible-playbook -i $(INVENTAIRE_PRODUCTION) playbooks/maintenance/emancipation-prouver.yml \ -e emancipation_service=$(SERVICE) \ $(if $(HOTE),-e emancipation_hotes=$(HOTE)) $(ARGS) .PHONY: sonder sonder: ## Sonde une cible et DIT ce qui distingue — CIBLE= [PORTS="22 443"] @if [[ -z "$(CIBLE)" ]]; then \ printf '%s\n' 'Refus: relancer avec CIBLE=.'; \ printf '%s\n' 'Ex. make sonder CIBLE=infra-dns-01 PORTS="53 22"'; \ exit 2; \ fi @python3 scripts/sonder.py $(CIBLE) $(or $(PORTS),22) $(if $(DELAI),--delai $(DELAI)) .PHONY: verifier verifier: lint test inventaire-verifier site-verifier flux-verifier syntaxe ## Rejoue les preuves SANS reecrire le rapport (verification rapide) python3 scripts/prouver.py --verifier # Ce que la CI execute, et que tu peux rejouer a l'identique. # # Le modele PUBLIC est monte comme instance jetable et vise par SETOPS_INSTANCE / # SETOPS_UNDERLAY : AUCUN symlink n'est touche, ton instance reste montee pendant # l'execution. Deux details ne sont pas negociables, mesures le 2026-08-20 : # # - l'instance jetable est un DOSSIER FRERE du depot, pas un /tmp. La federation se # decouvre par les dossiers freres (`devis_reseau.decouvrir`) : ailleurs, elle est # invisible, et quatre preuves tombent en disant « aucun tenant federe decouvert » ; # - `env -u SETOPS_INVENTAIRE`, sinon la variable exportee par CE make survit dans le # sous-make et designe l'inventaire de l'instance ACTIVE — le piege de P03. # # Et SETOPS_UNDERLAY n'est pose QUE pour la verification, jamais pour l'application : # l'inventaire de l'instance jetable doit etre ecrit avec la meme fabric que celle avec # laquelle P03 le REGENERERA ensuite. En posant l'underlay du modele des l'application, # `make ci` passait sur un depot nu et echouait chez un exploitant dont le symlink # `underlay.yml` designe une autre fabric — quatre hotes « en ecart », pour un ecart # fabrique par la commande elle-meme. # LE GENOME : les depots sans lesquels cet ecosysteme ne peut pas etre refabrique, et la # PARENTE qu'il inscrit (de quel moteur il descend, a quel commit, sous quelle etiquette # signee). Perdre les VM coute du temps ; perdre ces depots coute l'ecosysteme. .PHONY: genome genome-inscrire genome-verifier genome: ## Les depots requis a la reproduction de cet ecosysteme, et leur etat python3 scripts/genome.py lister genome-inscrire: ## Inscrit la parente dans l'instance (instance/parente.yml) python3 scripts/genome.py inscrire genome-verifier: ## La parente inscrite tient-elle encore ? (code de sortie) python3 scripts/genome.py verifier .PHONY: ci ci: ## Verifie le depot comme la CI : modele public monte a l'ecart, sans toucher ton instance @set -e ; \ frere="../instance-ci-$$$$" ; \ trap 'rm -rf "$$frere"' EXIT ; \ rm -rf "$$frere" ; cp -r exemples/modeles/socle "$$frere" ; \ env -u SETOPS_INVENTAIRE SETOPS_INSTANCE="$$frere" \ $(MAKE) --no-print-directory instancier-appliquer FORCE=1 ; \ env -u SETOPS_INVENTAIRE SETOPS_INSTANCE="$$frere" \ SETOPS_UNDERLAY="$$frere/underlay.yml" \ $(MAKE) --no-print-directory verifier # Harnais de preuve : rejoue les preuves automatisables du registre et ecrit # docs/audit/preuve-.md (piece justificative horodatee, rejouable). # `make verifier` l'appelle en mode --verifier (preuves seules, aucun rapport ecrit). .PHONY: prouver prouver: ansible-runtime _instance-requise ## Execute les preuves et ecrit docs/audit/preuve-.md python3 scripts/prouver.py .PHONY: inventaire hote-planifier hote-ajouter hote-groupes hote-afficher appliquer deployer deployer-groupe cloner-vm creer-vm config inventaire-ui inventaire-verifier inventaire-lister inventaire-graphe inventaire-hote inventaire-lab inventaire-production instance-utiliser instance-courante inventaire: inventaire-production ## Verifie l'inventaire et en affiche le graphe (lab puis production) # Bascule le symlink 'instance' vers un autre dépôt d'instance (séparation par # instance : prod vs bac à sable). Ex. : make instance-utiliser NOM=OPS-Chezlepro-lab instance-utiliser: ## Bascule l'instance active (symlink instance/) vers un dossier frere — NOM= @if [[ -z "$(NOM)" ]]; then printf '%s\n' "Usage: make instance-utiliser NOM= (ex. OPS-Chezlepro-lab)"; exit 2; fi @if [[ ! -d "../$(NOM)" ]]; then printf '%s\n' "Introuvable: ../$(NOM)"; exit 2; fi @if [[ -e instance && ! -L instance ]]; then printf '%s\n' "Refus: 'instance' existe et n'est pas un symlink."; exit 2; fi @rm -f instance && ln -s "../$(NOM)" instance @printf 'instance -> %s\n' "$$(readlink instance)" instance-courante: ## Affiche vers quel ecosysteme pointe l'instance active @printf 'instance -> %s\n' "$$(readlink instance 2>/dev/null || echo '(non monté)')" # Vue d'ensemble : toutes les instances de la fédération, l'active (*), leur index, # plage VLAN, statut fédéré/prod ; signale les collisions d'index. Lecture seule. instances: ## Liste les ecosystemes decouverts (dossiers freres) et signale les collisions d'index @python3 scripts/instances.py # (Re)génère le plan de recette (docs/audit/plan-de-recette.md) depuis les exercices # du wiki. La preuve P22 vérifie qu'il reste à jour. plan-recette: ## Regenere docs/audit/plan-de-recette.md depuis le wiki @python3 scripts/plan_recette.py # Liste les modèles disponibles (socle + SETOPS_MODELES) pour créer une instance. instance-modeles: ## Liste les modeles d'ecosysteme disponibles @python3 scripts/instance_creer.py --lister-modeles # Crée un dépôt d'instance frère depuis un modèle. Ne bascule pas le symlink. # Ex. : make instance-creer NOM=OPS-ClientX MODELE=socle INDEX=4 instance-creer: ## Cree un nouvel ecosysteme depuis un modele — NOM= MODELE= @python3 scripts/instance_creer.py --nom "$(NOM)" --modele "$(MODELE)" \ $(if $(INDEX),--index $(INDEX),) # Crée un MODÈLE (dépôt privé). Deux modes : # base : copier un modèle générique -> make model-creer MODE=base BASE=identite NOM=maison-obnl # instance : promouvoir une instance -> make model-creer MODE=instance SOURCE=OPS-Chezlepro NOM=cabinet model-creer: ## Cree un modele d'ecosysteme — MODE= NOM= @python3 scripts/model_creer.py --mode "$(MODE)" --nom "$(NOM)" \ $(if $(BASE),--base $(BASE),) $(if $(SOURCE),--source $(SOURCE),) $(if $(DEST),--dest $(DEST),) config: ## Affiche la configuration Proxmox lue par le moteur python3 scripts/config_proxmox.py inventaire-ui: _instance-requise ## Ouvre la console d'exploitation (GUI web) sur l'inventaire actif python3 scripts/inventory_gui.py --inventaire $(FICHIER_INVENTAIRE) hote-ajouter hote-planifier hote-groupes: @printf '%s\n' 'Cible depreciee: l inventaire est GENERE depuis le plan, il ne s edite plus a la main.' @printf '%s\n' 'Declare ou modifie l hote dans instance/plan/serveurs.yml (ou la vue Serveurs du GUI), puis :' @printf '%s\n' ' make instancier-appliquer' @printf '%s\n' '(creer-vm lit desormais VMID/IP/VLAN/passerelle directement dans l inventaire genere.)' @exit 2 hote-afficher: ansible-runtime ## Affiche tout ce que le plan derive pour un hote — HOTE= @if [[ -z "$(HOTE)" ]]; then \ printf '%s\n' 'Refus: relancer avec HOTE=nom_hote.'; \ exit 2; \ fi python3 scripts/inventory_host.py --inventaire $(FICHIER_INVENTAIRE) afficher --hote $(HOTE) appliquer: ansible-runtime ## Applique un groupe a la flotte — GROUPE= @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)' # ATTENDRE PLUTOT QUE CONSTATER (2026-08-27). `deployer` faisait un `ping` unique # (`_verifier-acces-hote`). Depuis que `creer-vm` ne guette plus le SSH — il prouve la # materialisation par l'agent invite, seul geste que le runner du SITE ait le droit de # faire chez un tenant — c'est ICI qu'il faut attendre : enchainer `creer-vm` puis # `deployer` tomberait sinon sur une machine encore en cloud-init. On y gagne au passage # ce que le `ping` unique ne faisait pas : attendre que `dpkg` se libere, faute de quoi # la premiere couche echoue sur un verrou APT. # # CONTREPARTIE ASSUMEE : un nom d'hote errone ne fait plus echouer tout de suite, il # patiente ATTENTE_HOTE secondes. Echouer vite sur l'injoignable interdirait de chainer # la creation au deploiement — le geste central d'une reconstruction. deployer: _instance-requise ## Deploie un hote, couche par couche, dans l'ordre du graphe — HOTE= @set -e; \ if [[ -z "$(HOTE)" ]]; then \ printf '%s\n' 'Refus: relancer avec HOTE=nom_hote.'; \ exit 2; \ fi; \ python3 scripts/inventory_host.py --inventaire $(FICHIER_INVENTAIRE) verifier-actif --hote $(HOTE); \ python3 scripts/inventory_host.py --inventaire $(FICHIER_INVENTAIRE) --dependances $(FICHIER_DEPENDANCES) verifier-dependances-hote --hote $(HOTE); \ playbooks="$$(python3 scripts/inventory_host.py --inventaire $(FICHIER_INVENTAIRE) playbooks --hote $(HOTE) --dossier-playbooks $(DOSSIER_PLAYBOOKS_GROUPES))"; \ if [[ -z "$$playbooks" ]]; then \ printf '%s\n' 'Refus: aucun playbook applicable pour HOTE=$(HOTE).'; \ exit 2; \ fi; \ $(VAULT_UNE_FOIS) \ $(MAKE) --no-print-directory _attendre-hote LIMITE="$(HOTE)"; \ $(MAKE) _verifier-privileges-hote LIMITE="$(HOTE)"; \ for playbook in $$playbooks; do \ ansible-playbook -i $(INVENTAIRE_PRODUCTION) "$$playbook" --limit "$(HOTE)"; \ done; \ $(MAKE) verifier-hote LIMITE="$(HOTE)" .PHONY: site site-verifier deployer-tout site: ansible-runtime ## Regenere playbooks/site.yml depuis les couches et le graphe de dependances python3 scripts/orchestrer.py ecrire ansible-playbook -i $(INVENTAIRE_PRODUCTION) playbooks/site.yml --syntax-check site-verifier: ## Verifie que playbooks/site.yml correspond aux couches declarees python3 scripts/orchestrer.py verifier .PHONY: flux flux-verifier cles-recenser: ## Montre ce qui n'existe QUE sur ce poste (sans rien ecrire) python3 scripts/exporter_cles.py --recenser cles-exporter: ## Sort les cles du poste, chiffrees et relues — VERS= @# A LANCER SOI-MEME, PAS PAR UN AGENT. `gpg` demande une phrase de passe : elle ne @# doit passer ni par un journal, ni par le contexte d'un assistant. Taper la commande @# dans son propre terminal est la seule facon de s'en assurer. @if [ -z "$(VERS)" ]; then \ printf 'Refus: relancer avec VERS=.\n'; \ printf 'Ex. make cles-exporter VERS=/media/danallaire/CLE\n'; exit 2; fi python3 scripts/exporter_cles.py --vers "$(VERS)" .PHONY: cles-recenser cles-exporter depot-hors-site: ## Copie le depot de sauvegarde du site HORS du site — VERS= @# Le depot heberge la racine de l'AC, la forge du genome, et l'etat de CHAQUE @# locataire — sur une seule machine, dans un seul batiment. La copie est opaque @# (chiffree cote client) : elle se depose chez un pair sans rien lui demander @# d'autre que de la disponibilite. @if [ -z "$(VERS)" ]; then \ printf 'Refus: relancer avec VERS=.\n'; \ printf 'Ex. make depot-hors-site VERS=/media/danallaire/setops-coffre\n'; exit 2; fi @# EN JSON, PAS EN `-e cle=valeur` (2026-09-05). La forme `cle=valeur` DECOUPE SUR @# LES BLANCS — c'est ainsi qu'on passe plusieurs variables d'un coup. Un chemin @# contenant une espace y perd donc tout ce qui suit le premier blanc, et l'erreur @# parle d'un repertoire introuvable au nom tronque : « /media/danallaire/Cle ». @# Le depot avait deja appris ca pour le clonage de VM ; la lecon ne s'etait pas @# propagee ici. Quatrieme fois que ce piege est paye. ansible-playbook -i scripts/site_inventaire.py \ playbooks/maintenance/depot-hors-site.yml -e '{"vers": "$(VERS)"}' .PHONY: depot-hors-site cles-compagnons: ## Depose le script de restauration sur une cle DEJA faite — VERS= @# POUR UNE CLE EXISTANTE. L'archive n'est pas refaite : ni phrase de passe redemandee, @# ni risque d'ecraser ce qui est deja verifie. @if [ -z "$(VERS)" ]; then \ printf 'Refus: relancer avec VERS=.\n'; exit 2; fi python3 scripts/exporter_cles.py --vers "$(VERS)" --compagnons cles-restaurer: ## Remet les cles en place depuis une archive — ARCHIVE= @# EN VRAI, ON N'UTILISE PAS CETTE CIBLE. Le jour ou l'on restaure, le depot n'est pas @# la — le cloner demande la cle SSH qu'on essaie justement de recuperer. On lance @# alors `restaurer_cles.py` DEPUIS LA CLE USB, ou les trois commandes du LISEZ-MOI. @# Cette cible existe pour eprouver la manoeuvre pendant qu'on a encore le choix. @if [ -z "$(ARCHIVE)" ]; then \ printf 'Refus: relancer avec ARCHIVE=.\n'; exit 2; fi python3 scripts/restaurer_cles.py "$(ARCHIVE)" $(if $(FORCER),--forcer) .PHONY: cles-compagnons cles-restaurer remise-recenser: ## Ce qu'une remise au client emporterait, sans rien ecrire python3 scripts/remise.py recenser remise-paquet: ## Temps 1 : fabrique le paquet de remise, chiffre et relu — VERS= @# A LANCER SOI-MEME, PAS PAR UN AGENT. `gpg` demande une phrase de passe : elle ne @# doit passer ni par un journal, ni par le contexte d'un assistant — et c'est elle @# qui voyage par un AUTRE canal que le support. @if [ -z "$(VERS)" ]; then \ printf 'Refus: relancer avec VERS=.\n'; \ printf 'Ex. make remise-paquet VERS=/media/danallaire/REMISE\n'; exit 2; fi python3 scripts/remise.py paquet --vers "$(VERS)" $(if $(SUPPORT_CHIFFRE),--support-chiffre) remise-inscrire: ## Inscrit la remise chez le locataire — RECU_PAR="…" COURRIEL="…" [DANS=30] @if [ -z "$(RECU_PAR)" ] || [ -z "$(COURRIEL)" ]; then \ printf 'Refus: relancer avec RECU_PAR="Prenom Nom" COURRIEL="…".\n'; \ printf 'Le responsable designe (D-18) se NOMME au moment de la remise.\n'; exit 2; fi python3 scripts/remise.py inscrire --recu-par "$(RECU_PAR)" --courriel "$(COURRIEL)" \ $(if $(DANS),--dans "$(DANS)",) remise-verifier: ## Etat de la remise : temps 1 fait ? temps 2 du, ou echu ? python3 scripts/remise.py verifier remise-recleer: ## Temps 2 : mesure la revocation reelle, puis estampille — CONFIRMER=true @# IL MESURE AVANT D'ESTAMPILLER. Un registre qui dirait « revoque » pendant que le @# plan garde la cle de l'hebergeur flatterait tout le monde : c'est le seul mensonge @# que ce fichier puisse porter sans que personne ne s'en apercoive. python3 scripts/remise.py recleer $(if $(filter true,$(CONFIRMER)),--confirmer) .PHONY: remise-recenser remise-paquet remise-inscrire remise-verifier remise-recleer cacher-paquets: ## Tire les paquets NON DEBIAN dans le cache du controleur (a faire en ligne) @# CE QUI REND LA RECONSTRUCTION HORS LIGNE POSSIBLE. Trois depots tiers sont en @# HTTPS et contournent le cache du site (`Acquire::https::Proxy "DIRECT"`). Sans @# cette cible, chaque VM neuve va chercher `step-cli` et `alloy` sur Internet — deux @# integrations UNIVERSELLES. A lancer depuis un poste connecte ; le deploiement, lui, @# n'en aura plus besoin. python3 scripts/cacher_paquets.py .PHONY: flux flux: ansible-runtime ## Regenere le registre des flux et les regles nftables depuis les meta/flux.yml python3 scripts/resoudre_flux.py registre python3 scripts/resoudre_flux.py nftables @# Les machines du SITE aussi : leur inventaire est dynamique, donc invisible @# pour la generation qui lit un `hosts.yml`. Deux absences se cachaient l'une @# l'autre — aucune regle generee, et aucun pare-feu applique pour s'en rendre @# compte. Sans underlay monte, la cible ne fait rien plutot que d'echouer. @python3 scripts/resoudre_flux.py nftables-site || printf 'note : pas de site monte, aucune regle de site generee.\n' .PHONY: devis-reseau devis-reseau: ansible-runtime ## Devis switch (VLANs/SVIs/ACLs) du reseau converge, derive des nomenclatures. DIALECTE=cisco|binardat python3 scripts/devis_reseau.py $(if $(DIALECTE),--dialecte $(DIALECTE),) .PHONY: devis-opnsense devis-opnsense-verifier devis-opnsense: ansible-runtime ## Devis OPNsense (frontiere nord/sud), derive du registre des flux (pair: externe) python3 scripts/devis_opnsense.py $(if $(JSON),--json,) .PHONY: proxmox-fw-plan proxmox-fw-appliquer proxmox-fw-plan: ansible-runtime ## Ecart entre le pare-feu est-ouest Proxmox et son devis (aucune ecriture) python3 scripts/appliquer_proxmox_fw.py proxmox-fw-appliquer: ansible-runtime ## Reconcilie le pare-feu est-ouest : IPSets, groupes, affectations. CONFIRMER=true @if [[ "$(CONFIRMER)" != "true" ]]; then \ printf '%s\n' "Refus: cette cible ECRIT sur le pare-feu du cluster (IPSets, groupes, VM)" \ "et RETIRE ce qui est perime. Elle n'active JAMAIS le pare-feu du datacenter." \ "Relire d'abord 'make proxmox-fw-plan', puis: make proxmox-fw-appliquer CONFIRMER=true"; \ exit 2; fi CONFIRMER=true python3 scripts/appliquer_proxmox_fw.py .PHONY: placement-plan sdn-plan sdn-appliquer placement-plan: ansible-runtime ## Le noeud, stockage, pont et gabarit du tenant existent-ils sur ce cluster ? (aucune ecriture) python3 scripts/devis_placement.py sdn-plan: ansible-runtime ## Ecart entre le SDN EVPN (+ sortie des VRF) et son devis (aucune ecriture) python3 scripts/appliquer_sdn.py sdn-appliquer: ansible-runtime ## Reconcilie le SDN : cree ce qui manque, RETIRE ce qui est perime. CONFIRMER=true @if [[ "$(CONFIRMER)" != "true" ]]; then \ printf '%s\n' "Refus: cette cible ECRIT sur le cluster (zones, VNets, sous-reseaux) et sur" \ "les noeuds de sortie (/etc/frr/frr.conf.local), et RETIRE ce qui est perime." \ "Relire d'abord 'make sdn-plan', puis: make sdn-appliquer CONFIRMER=true"; \ exit 2; fi CONFIRMER=true python3 scripts/appliquer_sdn.py .PHONY: ca-racine # Ou deposer la racine. Par defaut le repertoire courant : c'est un certificat # PUBLIC, pas un secret — il n'a rien a faire dans la voute, et tout a faire dans # le magasin de confiance de qui administre. CA_RACINE_DEST ?= ./root_ca.crt ca-racine: ansible-runtime _instance-requise ## Recupere la racine de l'AC interne + son empreinte (a verifier AVANT de l'installer) @set -e; \ hote="$$(python3 -c "import yaml,sys; d=yaml.safe_load(open('$(FICHIER_INVENTAIRE)')); \ import itertools; \ g=d['all']['children']; \ print(next(iter((g.get('serveur_step_ca') or {}).get('hosts') or {}), ''))" 2>/dev/null || true)"; \ if [[ -z "$$hote" ]]; then \ printf '%s\n' "Refus: aucun hote ne porte 'serveur_step_ca' dans $(FICHIER_INVENTAIRE)." \ "Cette instance n'a pas d'autorite de certification interne."; exit 2; fi; \ ansible -i $(FICHIER_INVENTAIRE) "$$hote" --become \ -m fetch -a "src=/etc/step-ca/certs/root_ca.crt dest=$(CA_RACINE_DEST) flat=yes" >/dev/null; \ printf '%s\n' "Racine de l'AC ecrite dans $(CA_RACINE_DEST) (depuis $$hote)."; \ printf '%s\n' "" " sujet : $$(openssl x509 -in $(CA_RACINE_DEST) -noout -subject | sed 's/^subject=//')"; \ printf '%s\n' " valide : $$(openssl x509 -in $(CA_RACINE_DEST) -noout -enddate | sed 's/^notAfter=//')"; \ printf '%s\n' " empreinte: $$(openssl x509 -in $(CA_RACINE_DEST) -noout -fingerprint -sha256 | sed 's/^.*=//' | tr -d ':' | tr 'A-Z' 'a-z')"; \ printf '%s\n' "" \ "COMPARE l'empreinte avec celle de l'AC avant de l'installer :" \ " make ca-empreinte" \ "" \ "Installer une AC, c'est lui donner le droit de signer N'IMPORTE QUEL nom" \ "pour ton navigateur. La comparaison est ce qui distingue ta racine d'une" \ "racine interceptee — ce n'est pas une formalite." \ "" \ " sudo cp $(CA_RACINE_DEST) /usr/local/share/ca-certificates/setops-root.crt" \ " sudo update-ca-certificates" \ " (Firefox a son propre magasin : Parametres > Certificats > Autorites)" .PHONY: ca-empreinte ca-empreinte: ansible-runtime _instance-requise ## Empreinte de la racine, lue SUR l'AC (le temoin de comparaison) @set -e; \ hote="$$(python3 -c "import yaml; d=yaml.safe_load(open('$(FICHIER_INVENTAIRE)')); \ g=d['all']['children']; \ print(next(iter((g.get('serveur_step_ca') or {}).get('hosts') or {}), ''))" 2>/dev/null || true)"; \ if [[ -z "$$hote" ]]; then \ printf '%s\n' "Refus: aucun hote ne porte 'serveur_step_ca'."; exit 2; fi; \ ansible -i $(FICHIER_INVENTAIRE) "$$hote" --become -m command \ -a "step certificate fingerprint /etc/step-ca/certs/root_ca.crt" 2>/dev/null \ | tail -1 | tr -d ' \r' .PHONY: frontiere-plan frontiere-appliquer frontiere-mesurer mtu-mesurer versions-mesurer courriel-plan: ansible-runtime ## Chaine Postfix -> LDAP -> Dovecot -> IMAP (aucune ecriture) @rm -f $(SETOPS_INSTANCE)/devis-courriel.json.* @ansible-playbook -i $(SETOPS_INVENTAIRE) playbooks/maintenance/devis-courriel.yml >/dev/null @python3 scripts/devis_courriel.py postgresql-plan: ansible-runtime ## Chiffrement impose et portee des acces PostgreSQL (aucune ecriture) @rm -f $(SETOPS_INSTANCE)/devis-postgresql.json.* @ansible-playbook -i $(SETOPS_INVENTAIRE) playbooks/maintenance/devis-postgresql.yml >/dev/null @python3 scripts/devis_postgresql.py expositions-plan: ansible-runtime ## Chaque exposition du plan repond-elle ? (edge et poste, aucune ecriture) @ansible-playbook -i $(SETOPS_INVENTAIRE) playbooks/maintenance/devis-expositions.yml >/dev/null @python3 scripts/devis_expositions.py versions-mesurer: ## De combien nos epinglages ont-ils vieilli ? (interroge les amonts, aucune ecriture) @python3 scripts/devis_versions.py mtu-mesurer: ansible-runtime ## L'invite porte-t-il le MTU de sa zone SDN ? (aucune ecriture) @ansible-playbook -i $(SETOPS_INVENTAIRE) playbooks/maintenance/devis-mtu.yml >/dev/null @python3 scripts/devis_mtu.py frontiere-mesurer: ansible-runtime ## La frontiere refuse-t-elle ce qui n'est pas declare ? (sonde reelle, aucune ecriture) @ansible-playbook -i $(SETOPS_INVENTAIRE) playbooks/maintenance/devis-frontiere.yml >/dev/null @python3 scripts/devis_frontiere.py certificats-plan: ansible-runtime ## Ecart entre les certificats sur disque et ceux reellement servis (aucune ecriture) @rm -f $(SETOPS_INSTANCE)/devis-certificats.json.* @ansible-playbook -i $(SETOPS_INVENTAIRE) playbooks/maintenance/devis-certificats.yml >/dev/null @python3 scripts/devis_certificats.py ports-verifier: ## Deux roles co-localises revendiquent-ils le meme port ? (lecture seule) @python3 scripts/verifier_ports.py intrants-verifier: ## Tout intrant qu'un role EXIGE est-il fourni par l'instance ? (lecture seule) @python3 scripts/verifier_intrants.py identite-plan: ansible-runtime ## Ecart entre l'identite deployee et ce que le plan derive (aucune ecriture) @ansible-playbook -i $(SETOPS_INVENTAIRE) playbooks/maintenance/devis-identite.yml >/dev/null @python3 scripts/devis_identite.py frontiere-plan: ansible-runtime ## Ecart entre la frontiere OPNsense et son devis (aucune ecriture) python3 scripts/appliquer_opnsense.py frontiere-appliquer: ansible-runtime ## Reconcilie la frontiere : cree ce qui manque, RETIRE ce qui est perime. CONFIRMER=true @if [[ "$(CONFIRMER)" != "true" ]]; then \ printf '%s\n' "Refus: cette cible ECRIT sur le pare-feu de bordure et RETIRE les regles perimees." \ "Relire d'abord 'make frontiere-plan', puis: make frontiere-appliquer CONFIRMER=true"; \ exit 2; fi CONFIRMER=true python3 scripts/appliquer_opnsense.py .PHONY: devis-proxmox-fw devis-proxmox-fw-verifier devis-proxmox-fw: ansible-runtime ## Devis pare-feu Proxmox (est-ouest intra-tenant), derive du registre des flux python3 scripts/devis_proxmox_fw.py $(if $(JSON),--json,) devis-proxmox-fw-verifier: ## Verifie le devis du pare-feu est-ouest Proxmox (aucune ecriture) python3 scripts/devis_proxmox_fw.py --verifier .PHONY: site-intrants site-intrants-verifier site-intrants: ## Les intrants que le SITE monte expose a ses locataires (derives) python3 scripts/site_intrants.py $(if $(JSON),--json,) site-intrants-verifier: ## Le locataire monte suit-il encore son site ? (aucune ecriture) python3 scripts/site_intrants.py --verifier .PHONY: devis-proxmox-pools devis-proxmox-pools-verifier devis-proxmox-pools: ansible-runtime ## Devis des pools Proxmox (un par tenant), derive du plan python3 scripts/devis_proxmox_pools.py $(if $(JSON),--json,) devis-proxmox-pools-verifier: ## Verifie le devis des pools Proxmox (aucune ecriture) python3 scripts/devis_proxmox_pools.py --verifier .PHONY: devis-sdn devis-sdn-verifier devis-sdn: ansible-runtime ## Devis SDN EVPN (zone + VNets + sous-reseaux par tenant), derive du seed python3 scripts/devis_sdn.py $(if $(JSON),--json,) devis-sdn-verifier: ## Verifie le devis SDN EVPN — zones, VNets, sous-reseaux (aucune ecriture) python3 scripts/devis_sdn.py --verifier devis-opnsense-verifier: ## Verifie le devis de la frontiere nord/sud (aucune ecriture) python3 scripts/devis_opnsense.py --verifier .PHONY: underlay underlay: ## Underlay (fabric physique cluster-global : mgmt/iSCSI/Ceph) : affiche + valide (P23) python3 scripts/underlay.py underlay-plan: ## Confronte l'underlay DECLARE au reel (API du cluster + sonde TCP) — n'ecrit rien python3 scripts/devis_underlay.py flux-verifier: ## Verifie que le registre des flux correspond aux meta/flux.yml des roles python3 scripts/resoudre_flux.py verifier .PHONY: valider valider: ansible-runtime ## Passe la recette de validation sur la flotte ansible-playbook -i $(INVENTAIRE_PRODUCTION) playbooks/valider.yml .PHONY: schema schema: ## Regenere docs/audit/schema-plan.json depuis les registres et les validateurs python3 scripts/schema_plan.py .PHONY: wiki-publier wiki-publier: ## Publie le wiki (wiki/) vers la forge @# CHAQUE RUNNER PUBLIE POUR SA FORGE (2026-09-14). Sans `WIKI_REMOTE`, l'adresse se @# DERIVE de l'ecosysteme monte : le nom que son plan expose pour `serveur_forgejo`. @# @# DEUX NOTIONS A NE PAS CONFONDRE. La forge AMONT est celle ou l'on LIT le genome ; MA @# forge est celle ou l'on SERT les siens. Le runner d'un locataire lit chez son @# hebergeur et sert chez lui — un employe de TechnoLibre lit la forge de TechnoLibre. @# @# `WIKI_REMOTE` L'EMPORTE TOUJOURS, et c'est necessaire : le poste du mainteneur n'est @# le runner d'aucun ecosysteme, et publie vers le domicile PUBLIC du projet, qui n'est @# la forge d'aucun plan. @# @# `$(WIKI_REMOTE)`, PAS `$$remote` — CORRIGE LE 2026-09-14. La ligne lisait une variable @# de SHELL homonyme, que rien ne definit. `WIKI_REMOTE` etait documente CINQ fois dans @# ce Makefile, dont dans le message d'erreur qui le proposait comme remede — et la @# cible ne l'a jamais lu. L'echappatoire documentee n'existait pas. @# @# `WIKI_BRANCHE`, deux cibles plus bas, etait ecrit correctement depuis le debut. C'est @# en les mettant cote a cote que ca se voit : une option qui se lit autrement que sa @# voisine est l'endroit ou regarder. @set -e; \ remote="$(WIKI_REMOTE)"; \ if [[ -z "$$remote" ]]; then \ remote="$$(python3 scripts/ma_forge.py 2>/dev/null || true)"; \ fi; \ if [[ -z "$$remote" ]]; then \ printf '%s\n' 'Refus: aucune forge — ni WIKI_REMOTE, ni derivable du plan monte.'; \ python3 scripts/ma_forge.py >/dev/null || true; \ printf '%s\n' 'Ex: make wiki-publier WIKI_REMOTE=ssh://git@forge.//.wiki.git'; \ exit 2; \ fi; \ printf 'Forge visee : %s\n' "$$remote"; \ src="$(CURDIR)/wiki"; \ tmp="$$(mktemp -d)"; \ trap 'rm -rf "$$tmp"' EXIT; \ printf '%s\n' "Clonage du wiki: $$remote"; \ if ! git clone --quiet --depth 1 "$$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; \ : ' UN WIKI VIDE SE CLONE TRES BIEN, et le garde-fou ci-dessus ne le voyait pas :'; \ : ' il ne teste que l ECHEC du clone, alors que son message parle du cas VIDE.'; \ : ' Consequence mesuree le 2026-09-08 contre un depot bare local : la recette'; \ : ' allait au bout et creait une branche `master` — parce que `init.defaultBranch`'; \ : ' vaut master sur ce poste — alors que le wiki deja publie vit sur `main`. Le nom'; \ : ' de branche etait DEVINE, et il divergeait d une forge a l autre.'; \ : ' On refuse donc plutot que de choisir a la place de Forgejo : creer une premiere'; \ : ' page dans son interface initialise le wiki avec LA branche qu il attend.'; \ : ' ON NE DEVINE PAS — ON DEMANDE. Forgejo DECLARE la branche qu il attend, dans'; \ : ' `wiki_branch` de son API : GET /api/v1/repos//. Mesure du'; \ : ' 2026-09-09 sur forge.genese.internal : `wiki_branch = main`. Le refus ci-dessous'; \ : ' reste le DEFAUT — depuis ce poste l API n est pas joignable (la forge du site'; \ : ' n accepte le 443 que des machines qui declarent le flux) — mais qui a la reponse'; \ : ' peut la donner, au lieu de passer par l interface web :'; \ : ' make wiki-publier WIKI_REMOTE=... WIKI_BRANCHE=main'; \ : ' C est la difference entre une garde qui protege et une garde qui bloque.'; \ if ! git -C "$$tmp/wiki" rev-parse --verify --quiet HEAD >/dev/null; then \ if [[ -n "$(WIKI_BRANCHE)" ]]; then \ printf '%s\n' "Wiki vide : initialisation sur la branche declaree « $(WIKI_BRANCHE) »."; \ git -C "$$tmp/wiki" checkout -q -b "$(WIKI_BRANCHE)"; \ else \ printf '%s\n' 'Refus: ce wiki est VIDE (aucun commit).' \ 'Publier ici inventerait un nom de branche — sur ce poste `master`, alors que' \ 'le wiki deja en service vit sur `main`. Deux issues :' \ ' - demander a Forgejo : GET /api/v1/repos// -> `wiki_branch`,' \ ' puis make wiki-publier WIKI_REMOTE=... WIKI_BRANCHE=' \ " - ou creer une premiere page dans l interface Forgejo de $$remote"; \ exit 2; \ fi; \ fi; \ find "$$tmp/wiki" -maxdepth 1 -name '*.md' -delete; \ for f in "$$src"/*.md; do \ bn="$$(basename "$$f")"; \ [[ "$$bn" == "README.md" ]] && continue; \ cp "$$f" "$$tmp/wiki/$$bn"; \ done; \ rm -rf "$$tmp/wiki/img"; \ if compgen -G "$$src/img/*-annote.svg" > /dev/null; then \ mkdir -p "$$tmp/wiki/img"; \ cp -L "$$src"/img/*-annote.svg "$$tmp/wiki/img/"; \ fi; \ cd "$$tmp/wiki"; \ sha="$$(git -C "$(CURDIR)" rev-parse --short HEAD 2>/dev/null || echo inconnu)"; \ if [[ -z "$$(git status --porcelain)" ]]; then \ printf '%s\n' 'Wiki deja a jour (aucun changement).'; \ else \ git add -A; \ git commit --quiet -m "Publication du wiki depuis le depot (source: $$sha)"; \ git push --quiet; \ printf '%s\n' 'Wiki publie.'; \ fi; \ : ' LE TEMOIN. Sans lui, l ecart entre wiki/ et la forge n est mesurable par'; \ : ' personne : il s est creuse de 27 jours en silence avant que P60 n existe.'; \ : ' ECRIT DANS LES DEUX BRANCHES, et la seconde est la moins evidente. « Deja a'; \ : ' jour » est precisement le cas ou le temoin doit dire que la forge est au'; \ : ' niveau du depot : sortir la sans rien deposer laissait P60 rouge apres une'; \ : ' publication reussie, ce qui apprend a ignorer la preuve. Paye une fois.'; \ : ' APRES le push : un temoin ne doit jamais dire qu on a publie si on a echoue.'; \ { printf '%s\n' '---' \ '# Ecrit par `make wiki-publier`, lu par la preuve P60. Ne pas editer a la main.' \ "remote: $$remote" \ "source: $$sha" \ "date: $$(date +%F)"; } > "$(CURDIR)/docs/audit/wiki-publie.yml"; \ printf '%s\n' 'Temoin depose : docs/audit/wiki-publie.yml (a committer).' deployer-tout: _instance-requise ## Deploie TOUTE la flotte dans l'ordre des couches @set -e; \ if [[ "$(CONFIRMER)" != "true" ]]; then \ printf '%s\n' 'Refus: deploiement ORCHESTRE de TOUTE la flotte (action impactante).'; \ printf '%s\n' 'Relancer avec CONFIRMER=true. Astuce: tester d abord en idempotent avec MODE_CHECK=1.'; \ exit 2; \ fi; \ python3 scripts/verifier_genome_a_jour.py || exit 2; \ 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:-}$${ANSIBLE_VAULT_IDENTITY_LIST:-}" ]]; then \ if [[ -t 0 ]]; then \ read -r -s -p 'Mot de passe du vault Ansible: ' mdp; echo; \ vf="$$(mktemp)"; printf '%s' "$$mdp" > "$$vf"; chmod 600 "$$vf"; \ export ANSIBLE_VAULT_PASSWORD_FILE="$$vf"; \ trap 'rm -f "$$vf"' EXIT; \ else \ printf '%s\n' 'Refus: vault chiffre detecte mais aucun mot de passe (entree non interactive). Fournir ANSIBLE_VAULT_PASSWORD_FILE ou le champ vault de la GUI.'; \ exit 2; \ fi; \ fi; \ $(MAKE) _verifier-acces-hote LIMITE="$(GROUPE_HOTES_ACTIFS)"; \ $(MAKE) _verifier-privileges-hote LIMITE="$(GROUPE_HOTES_ACTIFS)"; \ mode="$$([[ -n "$(MODE_CHECK)" ]] && printf -- '--check --diff' || true)"; \ ansible-playbook -i $(INVENTAIRE_PRODUCTION) playbooks/site.yml --limit "$(GROUPE_HOTES_ACTIFS)" $$mode # --- Reconstruction from-zero : creer TOUTES les VM (2a) puis deployer (2b) --- .PHONY: flotte-creer # 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: ===` en direct pour suivre l'avancement, # et le detail arrive ordonne. # # Un echec n'est pas avale : le code de retour de chaque hote est relu, et la cible sort # en erreur si l'un d'eux a echoue — sinon `_attendre-flotte` partirait sur une flotte # incomplete. # --- LE MOT DE PASSE DE LA VOUTE, DEMANDE UNE SEULE FOIS ---------------------- # # Deux cibles longues en ont besoin : `deployer-tout` (une invocation ansible) et # `flotte-creer` (une invocation PAR MACHINE). La seconde redemandait le mot de passe a # chaque VM — quinze fois sur la reconstruction de Chezlepro, quatre en parallele, avec # la sortie redirigee vers des journaux : l'exploitant est harcele par des invites qu'il # ne voit meme pas. # # UN SEUL BLOC POUR LES DEUX. Deux facons de manipuler un secret finiraient par diverger, # et c'est le genre de divergence qu'on ne remarque qu'apres. # # `-t 0` : ne demander que si un humain est au clavier. Un appel non interactif refuse # clairement plutot que de pendre sur une invite que personne ne lit. define VAULT_UNE_FOIS vault_chiffre="$$(grep -rlsIF '$$ANSIBLE_VAULT' $(dir $(INVENTAIRE_PRODUCTION))group_vars 2>/dev/null | head -1 || true)"; \ if [[ -n "$$vault_chiffre" && -z "$${ANSIBLE_VAULT_PASSWORD_FILE:-}$${ANSIBLE_VAULT_IDENTITY_LIST:-}" ]]; then \ if [[ -t 0 ]]; then \ read -r -s -p 'Mot de passe du vault Ansible (demande une seule fois): ' mdp; echo; \ vf="$$(mktemp)"; printf '%s' "$$mdp" > "$$vf"; chmod 600 "$$vf"; \ export ANSIBLE_VAULT_PASSWORD_FILE="$$vf"; \ trap 'rm -f "$$vf"' EXIT; \ else \ printf '%s\n' 'Refus: vault chiffre detecte mais aucun mot de passe (entree non interactive). Fournir ANSIBLE_VAULT_PASSWORD_FILE ou le champ vault de la GUI.'; \ exit 2; \ fi; \ fi; endef flotte-creer: _instance-requise ## Cree les VM manquantes de la flotte depuis le plan — PARALLELE=n @set -e; \ if [[ "$(CONFIRMER)" != "true" ]]; then \ printf '%s\n' 'Refus: creation de TOUTES les VM actives du plan (clone Proxmox).'; \ printf '%s\n' 'Relancer avec CONFIRMER=true.'; \ exit 2; \ fi; \ hotes="$$(python3 scripts/inventory_host.py --inventaire $(INVENTAIRE_PRODUCTION) lister-actifs)"; \ if [[ -z "$$hotes" ]]; then printf '%s\n' 'Refus: aucun hote actif dans le plan.'; exit 2; fi; \ par="$(PARALLELE)"; par="$${par:-4}"; \ tmp="$$(mktemp -d)"; trap 'rm -rf "$$tmp"' EXIT; \ $(VAULT_UNE_FOIS) \ printf 'Creation de %s VM, %s a la fois.\n' "$$(echo $$hotes | wc -w)" "$$par"; \ n=0; \ for h in $$hotes; do \ printf '\n=== Creation VM: %s ===\n' "$$h"; \ ( $(MAKE) creer-vm HOTE="$$h" > "$$tmp/$$h.log" 2>&1; printf '%s' "$$?" > "$$tmp/$$h.rc" ) & \ n=$$((n+1)); \ if (( n >= par )); then wait -n || true; n=$$((n-1)); fi; \ done; \ wait; \ echec=0; \ for h in $$hotes; do \ printf '\n----- journal de %s -----\n' "$$h"; \ cat "$$tmp/$$h.log" 2>/dev/null || printf '(aucune sortie)\n'; \ rc="$$(cat "$$tmp/$$h.rc" 2>/dev/null || printf 1)"; \ if [[ "$$rc" != 0 ]]; then printf '!! %s a ECHOUE (code %s)\n' "$$h" "$$rc"; echec=1; fi; \ done; \ if (( echec )); then printf '\nAu moins une VM n a pas ete creee — rien ne continue.\n'; exit 2; fi; \ printf '\nToutes les VM actives sont creees.\n' # --- LES MACHINES DU SITE ----------------------------------------------------- # # UN SITE N'EST PAS UN PLAN. Un tenant se derive de son `index` ; un site n'en a pas et # ne derive de rien — il EST le terrain sur lequel les tenants derivent. Ses machines se # declarent dans `underlay.yml`, a cote des switches et des hyperviseurs qui les portent. # # D'ou des cibles distinctes de `flotte-*` : elles ne demandent AUCUNE instance montee. # Le site existe avant tout tenant, et doit pouvoir naitre quand aucun n'est encore la. # # Le clone lui-meme reste `cloner-vm` — le seul chemin eprouve, et il n'y en aura pas # deux. `site_machines.py` ne fait que traduire la declaration en ses parametres. .PHONY: site-decrire site-decrire: ## Les machines de l'hebergeur declarees dans l'underlay @python3 scripts/site_machines.py decrire .PHONY: site-inventaire site-inventaire: ## L'inventaire dynamique du site, tel qu'Ansible le voit @ansible-inventory -i scripts/site_inventaire.py --graph .PHONY: site-appliquer site-appliquer: ## Applique un role aux machines du site — GROUPE= @# `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. @# @# LA VOUTE DU SITE est derivee du symlink `underlay.yml`, jamais redeclaree. Les roles @# partages exigent leurs secrets (Forgejo : secret_key, internal_token, admin_password). @# Un tenant les tient de `group_vars/all/vault.yml` ; le site les tient de @# `underlay.vault.yml`, qui vit a cote de sa carte et hors depot. Sans le `-e @`, le @# role echoue sur son assertion — et l'echec ne dit pas qu'il manque un FICHIER, @# seulement que les valeurs sont vides. @set -e; \ if [[ ! -f "playbooks/groupes/$(GROUPE).yml" ]]; then \ printf 'Refus: aucun playbook pour le groupe %s.\n' "$(GROUPE)"; exit 2; \ fi; \ connus="$$(python3 scripts/site_inventaire.py --list | python3 -c \ 'import json,sys; print(" ".join(k for k in json.load(sys.stdin) if k != "_meta"))')"; \ if [[ " $$connus " != *" $(GROUPE) "* ]]; then \ printf 'Refus: le groupe %s n existe pas au site.\n' "$(GROUPE)"; \ printf 'Groupes du site : %s\n' "$$connus"; \ exit 2; \ fi; \ voute="$$(dirname "$$(readlink -f underlay.yml)")/underlay.vault.yml"; \ supp=(); [[ -f "$$voute" ]] && supp+=( -e "@$$voute" ); \ ansible-playbook -i scripts/site_inventaire.py "playbooks/groupes/$(GROUPE).yml" \ "$${supp[@]}" $(ARGS) .PHONY: fiches fiches: ## Regenere la fiche de chaque role (docs/roles/) — ROLE= pour une seule @# CHAQUE ROLE DECLARE DEJA CE QU'IL FAUDRAIT DESSINER. La fiche ne fait que le lire : @# `meta/flux.yml` (qui lui parle), `supervision.yml` (quel verdict il rend), @# `metriques.yml` (quelles series il expose), `empreinte.yml` (ce qu'il coute), @# `authentification.yml` (qui entre). Chaque ligne porte la RAISON declaree. @# @# GENEREES, JAMAIS ECRITES. Soixante-huit pages faites a la main seraient perimees @# avant la fin du mois — c'est « une liste qui suit une autre prend du retard », et @# une soixante-neuvieme liste ne ferait pas exception. @# @# UNE SECTION VIDE EST UNE INFORMATION : un role sans sonde l'affiche, et l'ensemble @# devient la carte de ce qui reste a faire, tenue a jour toute seule. python3 scripts/fiche_role.py $(ROLE) .PHONY: site-deployer-tout site-deployer-tout: ansible-runtime ## Deploie TOUT le site dans l'ordre des couches — CONFIRMER=true @# LE SITE AVAIT L'ORCHESTRATION, PAS LE MOYEN DE LA LANCER (2026-09-14). @# @# `playbooks/site.yml` est genere par `orchestrer.py` et ordonne les couches pour @# N'IMPORTE QUELLE instance — son en-tete le dit : « le meme fichier sert toute @# instance ». Mais `deployer-tout` ne vise que l'inventaire d'un LOCATAIRE, et le @# site n'avait que `site-appliquer GROUPE=`. @# @# CE QUE CA COUTAIT : le site ne se DEPLOYAIT que groupe par groupe, a la main, dans @# un ordre qu'il fallait se rappeler. Un hebergeur qu'on ne peut remonter qu'en @# enchainant onze groupes de memoire n'est pas reconstructible — il est reparable par @# quelqu'un qui se souvient. C'est nommement l'une des trois limites du jalon de @# reconstruction autonome : « le SITE jamais reconstruit ». @# @# TROIS DIFFERENCES AVEC `deployer-tout`, ET AUCUNE N'EST COSMETIQUE : @# @# l'inventaire est un SCRIPT (`site_inventaire.py`), pas un fichier : un site se @# derive de son underlay, il ne se fige pas dans un `hosts.yml`. @# la voute vit a cote de la carte (`underlay.vault.yml`), hors depot, et non @# dans `group_vars/all`. Sans le `-e @`, les roles partages echouent @# sur une assertion qui ne dit pas qu'il manque un FICHIER. @# le perimetre est `hotes_actifs`, comme chez un locataire — les hyperviseurs et @# la frontiere sont dans l'inventaire mais ne se deploient pas. @set -e; \ if [[ "$(CONFIRMER)" != "true" ]]; then \ printf '%s\n' 'Refus: deploiement ORCHESTRE de TOUT le site (action impactante).'; \ printf '%s\n' 'Relancer avec CONFIRMER=true. Astuce: tester d abord en idempotent avec MODE_CHECK=1.'; \ exit 2; \ fi; \ python3 scripts/verifier_genome_a_jour.py || exit 2; \ python3 scripts/orchestrer.py verifier; \ python3 scripts/orchestrer.py ecrire; \ voute="$$(dirname "$$(readlink -f underlay.yml)")/underlay.vault.yml"; \ supp=(); [[ -f "$$voute" ]] && supp+=( -e "@$$voute" ); \ mode="$$([[ -n "$(MODE_CHECK)" ]] && printf -- '--check --diff' || true)"; \ ansible-playbook -i scripts/site_inventaire.py playbooks/site.yml \ --limit "$(GROUPE_HOTES_ACTIFS)" "$${supp[@]}" $$mode $(ARGS) # --- 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. # LE GABARIT PORTE-T-IL CE QUE LE SITE DECLARE DE LUI ? # # Son type de machine ne se corrige pas apres coup : convertir `i440fx` -> `q35` remplace # le materiel sous un systeme qui croit connaitre le sien, et chaque panne qui s'ensuit # ressemble a autre chose qu'a sa cause. On verifie donc la SOURCE — la propriete s'herite, # tous les clones d'un gabarit conforme le sont. # # A LA DEMANDE, pas dans `make prouver` : ce controle exige le cluster, que le harnais ne # suppose pas joignable. Meme nature que `genome-etat` et `underlay-plan`. .PHONY: routes-fabric-etat routes-fabric-etat: ansible-runtime ## Les hyperviseurs routent-ils toutes les zones du SITE ? — ne corrige rien @# UNE LISTE QUI DOIT SUIVRE UNE AUTRE LISTE PREND DU RETARD. Les routes specifiques @# de `vmbr0` s'arretaient a 10.0.34.0/24 quand le site en declarait six : les zones @# `sauvegarde` et `supervision` etaient nees sans que les routes suivent. Le symptome @# ne ressemblait pas a une route manquante — il ressemblait a un pare-feu. python3 scripts/routes_fabric_etat.py .PHONY: expositions-etat expositions-etat: ## Chaque exposition est-elle servie sous un certificat qui la porte ? — ne corrige rien @# `serveur_nginx` POSE LE VHOST ET LAISSE LE SAN EN ARRIERE. Mesure deux fois le @# 2026-09-10, sur `grafana -> observatoire` puis `icinga -> vigie` : la zone publiait @# le nouveau nom, le vhost repondait, le client OIDC l'acceptait — et le certificat @# servi portait encore l'ancien. C'est `client_pki` qui reemet, et rien ne le reclame. @# Ajouter `--site` pour l'ecosysteme du SITE. python3 scripts/expositions_etat.py $(if $(SITE),--site) .PHONY: gabarit-etat gabarit-etat: ## Le gabarit porte-t-il ce que le SITE declare ? — ne corrige rien, regarde python3 scripts/gabarit_etat.py .PHONY: genome-etat genome-etat: ## Ce que la forge du site porte, face au poste — ne corrige rien, regarde @set -e; \ voute="$$(dirname "$$(readlink -f underlay.yml)")/underlay.vault.yml"; \ supp=(); [[ -f "$$voute" ]] && supp+=( -e "@$$voute" ); \ ansible-playbook -i scripts/site_inventaire.py \ playbooks/maintenance/genome_etat.yml "$${supp[@]}" $(ARGS) .PHONY: genome-pousser genome-pousser: ## Le runner du site pousse le genome sur la forge du site — DEPOT= pour un seul @set -e; \ voute="$$(dirname "$$(readlink -f underlay.yml)")/underlay.vault.yml"; \ supp=(); [[ -f "$$voute" ]] && supp+=( -e "@$$voute" ); \ SETOPS_DEPOT="$(DEPOT)" ansible-playbook -i scripts/site_inventaire.py \ playbooks/maintenance/genome_pousser.yml "$${supp[@]}" $(ARGS) .PHONY: site-creer forge-amorcer: ## Amorce la forge d'un SITE neuf avec le genome — CONFIRMER=true @# LE MAILLON QUI N'EXISTAIT PAS (2026-09-12, premiere reconstruction du site depuis @# zero). Le runner clone le genome depuis SA PROPRE forge, et `serveur_forgejo` ne @# cree ni organisation ni depot : a froid la forge naît vide et le clone echoue. @# @# Le deploiement n'avait jamais rencontre ce cas — la forge existait depuis le premier @# jour, remplie a la main, a une date que personne n'a notee. @# @# DEPUIS LE POSTE, et c'est structurel : a cet instant il est le SEUL endroit qui @# detienne le genome. Un amorcage vient toujours de l'exterieur de ce qu'il amorce. @# @# export SETOPS_FORGE_MDP=… puis make forge-amorcer CONFIRMER=true python3 scripts/forge_amorcer.py $(if $(filter true,$(CONFIRMER)),--confirmer) \ $(if $(FORGE),--forge "$(FORGE)") .PHONY: forge-amorcer site-raser: ansible-runtime ## DESTRUCTIF : detruit les VM du SITE — CONFIRMER=true ET SITE= @# CETTE CIBLE MANQUAIT, ET SON ABSENCE AVAIT UN NOM (2026-09-12). `raser` ne vise que @# l'instance ACTIVE — un locataire. Rien ne detruisait les machines du site : ce n'est @# donc pas que personne n'avait essaye de le reconstruire depuis zero, c'est que @# l'outil n'en offrait pas le moyen. La limite « le site n'a jamais ete reconstruit » @# se lisait partout sans que sa CAUSE soit nommee. @# @# LE SITE PORTE LE DEPOT DE SAUVEGARDE DE CHAQUE LOCATAIRE et la racine de son AC. @# `make depot-hors-site VERS=` AVANT, toujours. python3 scripts/site_raser.py $(if $(filter true,$(CONFIRMER)),--confirmer) \ $(if $(SITE),--site "$(SITE)") $(if $(MACHINE),--machine "$(MACHINE)") .PHONY: site-raser site-creer: ## Cree les machines du site depuis l'underlay — CONFIRMER=true @set -e; \ if [[ "$(CONFIRMER)" != "true" ]]; then \ printf '%s\n' 'Refus: creation des VM de l hebergeur (clone Proxmox).'; \ printf '%s\n' 'Relancer avec CONFIRMER=true.'; \ exit 2; \ fi; \ machines="$$(python3 scripts/site_machines.py lister)"; \ if [[ -z "$$machines" ]]; then printf '%s\n' 'Refus: aucune machine declaree dans l underlay.'; exit 2; fi; \ for m in $$machines; do \ printf '\n=== Machine du site: %s ===\n' "$$m"; \ t0=$$(date +%s); \ params="$$(python3 scripts/site_machines.py parametres-proxmox --machine "$$m")"; \ eval "$$params"; \ $(MAKE) cloner-vm \ POOL="$$(python3 scripts/devis_proxmox_pools.py --pool-site)" \ HOTE="$$m" \ VMID="$$SETOPS_VMID" \ ADRESSE_IP="$$SETOPS_IP" \ CIDR="$$SETOPS_CIDR" \ PASSERELLE="$$SETOPS_PASSERELLE" \ VLAN="$${SETOPS_VLAN:-}" \ PONT_PROXMOX="$$SETOPS_PONT" \ STOCKAGE_PROXMOX="$${SETOPS_STOCKAGE:-$(STOCKAGE_PROXMOX)}" \ TAILLE_DISQUE="$$SETOPS_DISQUE" \ COEURS="$$SETOPS_COEURS" \ MEMOIRE="$$SETOPS_MEMOIRE" \ MEMOIRE_MIN="$$SETOPS_MEMOIRE_MIN" \ NOEUD_PROXMOX="$$SETOPS_NOEUD" \ VMID_MODELE="$${SETOPS_VMID_MODELE:-$(VMID_MODELE)}" \ CLONE_COMPLET="$$SETOPS_CLONE_COMPLET" \ FORMAT_DISQUE="$(FORMAT_DISQUE)"; \ printf '=== %s : %s s ===\n' "$$m" "$$(( $$(date +%s) - t0 ))"; \ done; \ printf '\nLes machines du site sont creees.\n' .PHONY: _attendre-flotte _attendre-flotte: ansible-runtime @set -e; \ max="$${ATTENTE_MAX:-600}"; deadline=$$(( $$(date +%s) + max )); \ printf 'Attente que la flotte reponde en SSH (max %ss)...\n' "$$max"; \ until ansible -i $(INVENTAIRE_PRODUCTION) $(GROUPE_HOTES_ACTIFS) -m ping -e ansible_become=false >/dev/null 2>&1; do \ if (( $$(date +%s) > deadline )); then printf 'Timeout: flotte injoignable apres %ss.\n' "$$max"; exit 1; fi; \ sleep 10; \ done; \ printf 'Flotte joignable.\n' .PHONY: reconstruire raser: ansible-runtime _instance-requise ## DESTRUCTIF : detruit les VM derivees du plan — exige CONFIRMER=true ET INSTANCE= @python3 scripts/raser.py $(if $(INSTANCE),--instance $(INSTANCE)) $(if $(HOTE),--hote $(HOTE)) $(if $(filter true,$(CONFIRMER)),--confirmer) # `make flux` D'ABORD, et ce n'est pas une precaution : sans lui, `instance/flux-genere/` # est VIDE et le socle deploie nftables en `policy drop` SANS AUCUNE REGLE DERIVEE. La # flotte se monte, SSH repond depuis l'administration — et tout le reste est mur. # # Invisible sur un ecosysteme deja construit, dont les .nft trainaient d'une execution # precedente. Trouve le 2026-08-10 en montant Technolibre depuis zero : l'AC etait # debout, son port 8443 en ecoute, et `step ca bootstrap` expirait depuis une VM du # meme sous-reseau. Zero regle pour 8443 dans le ruleset, `flux-genere/` vide. reconstruire: _instance-requise ## Reconstruit un ecosysteme depuis zero : VM puis deploiement complet @set -e; \ if [[ "$(CONFIRMER)" != "true" ]]; then \ printf '%s\n' 'Refus: RECONSTRUCTION — cree les VM manquantes (2a) PUIS deploie tout (2b).'; \ printf '%s\n' 'Idempotent : une VM deja presente est sautee (clone par nom), le deploiement converge.'; \ printf '%s\n' 'Relancer avec CONFIRMER=true.'; \ exit 2; \ fi; \ $(MAKE) flux; \ $(MAKE) flotte-creer CONFIRMER=true; \ $(MAKE) _attendre-flotte; \ $(MAKE) _amorcer-socle; \ $(MAKE) deployer-tout CONFIRMER=true # « Go ahead, make my day. » — LE bouton rouge : alias de reconstruire (Clint Eastwood). # Cree toutes les VM puis deploie tout, en une commande. Garde CONFIRMER=true. .PHONY: myDay # « Une PKI et un DNS fonctionnels avant toute chose » — contrainte d'architecture du # 2026-08-08. `deployer-tout` deroule par COUCHES : tous les hotes recoivent le socle, # puis tous recoivent la couche suivante. C'est correct, mais ca laisse chaque VM # reclamer un certificat a une autorite qui n'est pas encore debout — et l'echec se lit # alors comme un defaut du role, pas comme un defaut d'ordre. # # On monte donc l'AC puis le DNS COMPLETEMENT d'abord, hote par hote (`make deployer` # ordonne deja les groupes d'un hote par couches). Les deux se DERIVENT du plan : # deplacer l'autorite deplace l'amorcage avec elle. _amorcer-socle: ansible-runtime _instance-requise @set -e; \ for h in $$(python3 scripts/socle_amorcage.py); do \ printf '\n=== Amorcage du socle : %s ===\n' "$$h"; \ $(MAKE) deployer HOTE="$$h"; \ done; \ printf '\nPKI et DNS debout : le reste de la flotte peut monter.\n' myDay: reconstruire ## Alias strict de `reconstruire` (meme cible, memes gardes) deployer-groupe: ## Deploie un seul groupe sur toute la flotte — GROUPE= @if [[ -z "$(GROUPE)" ]]; then \ printf '%s\n' 'Refus: relancer avec GROUPE=nom_groupe.'; \ exit 2; \ fi $(MAKE) appliquer GROUPE="$(GROUPE)" .PHONY: verifier-deploiement verifier-deploiement: ansible-runtime ## Verifie l'etat de la flotte apres deploiement @set -e; \ if [[ -z "$(HOTE)" ]]; then \ printf '%s\n' 'Refus: relancer avec HOTE=nom_hote.'; \ exit 2; \ fi; \ python3 scripts/inventory_host.py --inventaire $(FICHIER_INVENTAIRE) verifier-actif --hote $(HOTE); \ python3 scripts/inventory_host.py --inventaire $(FICHIER_INVENTAIRE) --dependances $(FICHIER_DEPENDANCES) verifier-dependances-hote --hote $(HOTE); \ playbooks="$$(python3 scripts/inventory_host.py --inventaire $(FICHIER_INVENTAIRE) playbooks --hote $(HOTE) --dossier-playbooks $(DOSSIER_PLAYBOOKS_GROUPES))"; \ if [[ -z "$$playbooks" ]]; then \ printf '%s\n' 'Refus: aucun playbook applicable pour HOTE=$(HOTE).'; \ exit 2; \ fi; \ vault_chiffre="$$(grep -rlsIF '$$ANSIBLE_VAULT' $(dir $(INVENTAIRE_PRODUCTION))group_vars 2>/dev/null | head -1 || true)"; \ if [[ -n "$$vault_chiffre" && -z "$${ANSIBLE_VAULT_PASSWORD_FILE:-}$${ANSIBLE_VAULT_IDENTITY_LIST:-}" ]]; then \ if [[ -t 0 ]]; then \ read -r -s -p 'Mot de passe du vault Ansible: ' mdp; echo; \ vf="$$(mktemp)"; printf '%s' "$$mdp" > "$$vf"; chmod 600 "$$vf"; \ export ANSIBLE_VAULT_PASSWORD_FILE="$$vf"; \ trap 'rm -f "$$vf"' EXIT; \ else \ printf '%s\n' 'Refus: vault chiffre detecte mais aucun mot de passe (entree non interactive). Fournir ANSIBLE_VAULT_PASSWORD_FILE ou le champ vault de la GUI.'; \ exit 2; \ fi; \ fi; \ for playbook in $$playbooks; do \ ansible-playbook -i $(INVENTAIRE_PRODUCTION) "$$playbook" --limit "$(HOTE)" --check --diff; \ done cloner-vm: ansible-runtime ## Clone une VM depuis le gabarit dore — HOTE= VMID= @if [[ -z "$(HOTE)" || -z "$(VMID)" ]]; then \ printf '%s\n' 'Refus: relancer avec HOTE=nom VMID=id_clone.'; \ exit 2; \ fi @# En SDN EVPN, l'etiquette est portee par le VNet : `instancier` emet donc un VLAN @# VIDE et un pont derive (t17serv). Exiger un VLAN ici rejetait toute VM de tenant. @# Un VLAN vide n'est accepte QUE si un pont est fourni — sinon la VM n'aurait ni @# etiquette ni VNet, et se retrouverait branchee nulle part. @if [[ -z "$(VLAN)" && -z "$(PONT_PROXMOX)" ]]; then \ printf '%s\n' 'Refus: relancer avec VLAN=id_vlan, ou PONT_PROXMOX= en SDN.'; \ exit 2; \ fi @if [[ -n "$(VLAN)" ]] && { ! [[ "$(VLAN)" =~ ^[0-9]+$$ ]] || (( 10#$(VLAN) < 1 || 10#$(VLAN) > 4094 )); }; then \ printf '%s\n' 'Refus: VLAN doit etre un nombre entre 1 et 4094.'; \ exit 2; \ fi @if [[ -n "$(CLE_SSH_PUBLIQUE)" && ! -f "$(CLE_SSH_PUBLIQUE)" ]]; then \ printf '%s\n' 'Refus: cle publique SSH introuvable: $(CLE_SSH_PUBLIQUE)'; \ exit 2; \ fi @if [[ "$(DHCP)" != "true" && ( -z "$(ADRESSE_IP)" || -z "$(CIDR)" || -z "$(PASSERELLE)" ) ]]; then \ printf '%s\n' 'Refus: fournir ADRESSE_IP, CIDR et PASSERELLE, ou utiliser DHCP=true.'; \ exit 2; \ fi @ipconfig='ip=dhcp'; \ if [[ "$(DHCP)" != "true" ]]; then \ ipconfig='ip=$(ADRESSE_IP)/$(CIDR),gw=$(PASSERELLE)'; \ fi; \ extra_vars=( \ -e proxmox_clone_nom="$(HOTE)" \ -e proxmox_clone_vmid="$(VMID)" \ -e proxmox_clone_ipconfig0="$$ipconfig" \ -e proxmox_clone_pool="$(if $(POOL),$(POOL),$$(python3 scripts/devis_proxmox_pools.py --pool-actif))" \ ); \ [[ -n "$(VMID_MODELE)" ]] && extra_vars+=( -e proxmox_clone_vmid_modele="$(VMID_MODELE)" ); \ [[ -n "$(NOEUD_PROXMOX)" ]] && extra_vars+=( -e proxmox_clone_noeud="$(NOEUD_PROXMOX)" ); \ [[ -n "$(STOCKAGE_PROXMOX)" ]] && extra_vars+=( -e proxmox_clone_stockage="$(STOCKAGE_PROXMOX)" ); \ [[ -n "$(FORMAT_DISQUE)" ]] && extra_vars+=( -e proxmox_clone_format="$(FORMAT_DISQUE)" ); \ [[ -n "$(CLONE_COMPLET)" ]] && extra_vars+=( -e proxmox_clone_complet="$(CLONE_COMPLET)" ); \ [[ -n "$(TAILLE_DISQUE)" ]] && extra_vars+=( -e proxmox_clone_taille_disque="$(TAILLE_DISQUE)" ); \ [[ -n "$(COEURS)" ]] && extra_vars+=( -e proxmox_clone_coeurs="$(COEURS)" ); \ [[ -n "$(MEMOIRE)" ]] && extra_vars+=( -e proxmox_clone_memoire="$(MEMOIRE)" ); \ [[ -n "$(MEMOIRE_MIN)" ]] && extra_vars+=( -e proxmox_clone_memoire_min="$(MEMOIRE_MIN)" ); \ [[ -n "$(DISQUE_PROXMOX)" ]] && extra_vars+=( -e proxmox_clone_disque="$(DISQUE_PROXMOX)" ); \ [[ -n "$(DNS)" ]] && extra_vars+=( -e proxmox_clone_dns="$(DNS)" ); \ [[ -n "$(DOMAINE_RECHERCHE)" ]] && extra_vars+=( -e proxmox_clone_domaines_recherche="$(DOMAINE_RECHERCHE)" ); \ [[ -n "$(CIUSER)" ]] && extra_vars+=( -e proxmox_clone_ciuser="$(CIUSER)" ); \ [[ -n "$(CLE_SSH_PUBLIQUE)" ]] && extra_vars+=( -e proxmox_clone_cle_publique_fichier="$(CLE_SSH_PUBLIQUE)" ); \ [[ -n "$${SETOPS_CLES_AMORCAGE:-}" ]] && extra_vars+=( -e "$$(python3 -c 'import json,os; print(json.dumps({"proxmox_clone_cles_amorcage": os.environ["SETOPS_CLES_AMORCAGE"]}))')" ); \ [[ -n "$(PONT_PROXMOX)" ]] && extra_vars+=( -e proxmox_clone_pont="$(PONT_PROXMOX)" ); \ extra_vars+=( -e proxmox_clone_vlan="$(VLAN)" ); \ [[ -n "$(DEMARRER)" ]] && extra_vars+=( -e proxmox_clone_demarrer="$(DEMARRER)" ); \ vault_args=(); \ vault_file=""; \ for d in lab principal production; do \ for gv in group_vars/all/vault.yml group_vars/proxmox.vault.yml; do \ f="$(SETOPS_INSTANCE)/inventories/$$d/$$gv"; \ [[ -f "$$f" ]] && vault_file="$$f" && break 2; \ done; \ done; \ if [[ -z "$$vault_file" && -L underlay.yml ]]; then \ vault_file="$$(dirname "$$(readlink -f underlay.yml)")/underlay.vault.yml"; \ fi; \ if [[ -n "$$vault_file" && -f "$$vault_file" ]]; then \ read -r premiere_ligne < "$$vault_file" || true; \ case "$$premiere_ligne" in \ '$$ANSIBLE_VAULT'*) \ if [[ -z "$${ANSIBLE_VAULT_PASSWORD_FILE:-}$${ANSIBLE_VAULT_IDENTITY_LIST:-}" ]]; then \ vault_args+=( --ask-vault-pass ); \ fi; \ ;; \ esac; \ fi; \ ansible-playbook -i localhost, $(PLAYBOOK_PROXMOX_CLONER_VM) "$${vault_args[@]}" "$${extra_vars[@]}" creer-vm: _instance-requise ## Cree une VM et attend qu'elle soit joignable — HOTE= @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"; \ export SETOPS_CLES_AMORCAGE; \ $(MAKE) cloner-vm \ HOTE="$(HOTE)" \ VMID="$$SETOPS_VMID" \ ADRESSE_IP="$$SETOPS_IP" \ CIDR="$$SETOPS_CIDR" \ PASSERELLE="$$SETOPS_PASSERELLE" \ VLAN="$$SETOPS_VLAN" \ PONT_PROXMOX="$${SETOPS_PONT:-$(PONT_PROXMOX)}" \ STOCKAGE_PROXMOX="$${SETOPS_STOCKAGE:-$(STOCKAGE_PROXMOX)}" \ TAILLE_DISQUE="$${SETOPS_DISQUE:-$(TAILLE_DISQUE)}" \ COEURS="$${SETOPS_COEURS:-$(COEURS)}" \ MEMOIRE="$${SETOPS_MEMOIRE:-$(MEMOIRE)}" \ MEMOIRE_MIN="$${SETOPS_MEMOIRE_MIN:-$(MEMOIRE_MIN)}" \ NOEUD_PROXMOX="$${SETOPS_NOEUD:-$(NOEUD_PROXMOX)}" \ VMID_MODELE="$(VMID_MODELE)" \ FORMAT_DISQUE="$(FORMAT_DISQUE)" \ DISQUE_PROXMOX="$(DISQUE_PROXMOX)" \ DNS="$${SETOPS_DNS:-$(DNS)}" \ DOMAINE_RECHERCHE="$${SETOPS_DOMAINE:-$(DOMAINE_RECHERCHE)}" \ DHCP="$(DHCP)" \ CIUSER="$(CIUSER)" \ CLE_SSH_PUBLIQUE="$(CLE_SSH_PUBLIQUE)" \ DEMARRER="$(DEMARRER)" \ CLONE_COMPLET="$(CLONE_COMPLET)" @# `creer-vm` CONFIRME LA MATERIALISATION, PAS LA JOIGNABILITE (2026-08-27). Il @# attendait une reponse SSH — un geste que le runner du SITE ne peut pas faire : la @# frontiere lui refuse d'entrer chez les tenants, et c'est le sens meme de leur @# isolation. La premiere VM creee par le runner a donc ete declaree en echec apres @# 600 secondes alors qu'elle tournait, avec la bonne adresse. @# @# L'agent invite repond a cette question sans rien ouvrir — l'API des hyperviseurs @# est deja le flux par lequel la VM vient d'etre creee — et il PROUVE DAVANTAGE : @# il rapporte le systeme, le noyau et l'adresse effectivement portee, la ou SSH ne @# disait que « quelque chose ecoute sur le port 22 ». @# @# ATTENDRE LA DISPONIBILITE est un autre geste, deplace chez `deployer` : cloud-init @# et dpkg se guettent juste avant de configurer, par celui qui va configurer. @if [[ "$(ATTENDRE)" != "false" ]]; then \ params="$$(python3 scripts/inventory_host.py --inventaire $(INVENTAIRE_PRODUCTION) parametres-proxmox --hote $(HOTE))"; \ eval "$$params"; \ python3 scripts/attendre_materialisation.py \ --vmid "$$SETOPS_VMID" --ip "$$SETOPS_IP" --hote "$(HOTE)" \ --delai $(ATTENTE_HOTE); \ fi inventaire-verifier: ansible-runtime _instance-requise ## Verifie que l'inventaire se parse (voute dechiffree) ansible-inventory -i $(INVENTAIRE_LAB) --list > /dev/null ansible-inventory -i $(INVENTAIRE_PRODUCTION) --list > /dev/null python3 scripts/inventory_host.py --inventaire $(INVENTAIRE_PRODUCTION) verifier-playbooks --dossier-playbooks $(DOSSIER_PLAYBOOKS_GROUPES) python3 scripts/inventory_host.py --inventaire $(INVENTAIRE_PRODUCTION) --dependances $(FICHIER_DEPENDANCES) verifier-dependances --dossier-playbooks $(DOSSIER_PLAYBOOKS_GROUPES) python3 scripts/verifier_gui.py python3 scripts/serveurs.py verifier python3 scripts/applications.py verifier python3 scripts/bases_donnees.py verifier python3 scripts/domaines.py verifier .PHONY: bases bases-verifier domaines domaines-verifier applications applications-verifier applications-bootstrap serveurs serveurs-verifier serveurs-bootstrap serveurs: ## Liste les serveurs declares au plan python3 scripts/serveurs.py lister serveurs-verifier: ## Valide le registre des serveurs python3 scripts/serveurs.py verifier serveurs-bootstrap: ## REPRISE : (re)constitue plan/serveurs.yml depuis un inventaire existant python3 scripts/serveurs.py bootstrap .PHONY: instancier instancier-appliquer instancier: _instance-requise ## Genere hosts.yml depuis le plan (sans l'appliquer) python3 scripts/instancier.py generer python3 scripts/instancier.py comparer instancier-appliquer: _instance-requise ## Applique l'inventaire genere — FORCE=1 pour passer outre le diff python3 scripts/instancier.py appliquer $(if $(FORCE),--force) bases: ## Liste les bases de donnees declarees au plan python3 scripts/bases_donnees.py lister bases-verifier: ## Valide le registre des bases de donnees python3 scripts/bases_donnees.py verifier domaines: ## Liste les domaines declares au plan python3 scripts/domaines.py lister domaines-verifier: ## Valide le registre des domaines python3 scripts/domaines.py verifier applications: ## Liste les applications declarees au plan python3 scripts/applications.py lister applications-verifier: ## Valide le registre des applications python3 scripts/applications.py verifier applications-bootstrap: ## Amorce les applications declarees au plan python3 scripts/applications.py bootstrap inventaire-lister: ansible-runtime ## Affiche l'inventaire complet (JSON) ansible-inventory -i $(FICHIER_INVENTAIRE) --list inventaire-graphe: ansible-runtime ## Affiche le graphe des groupes de l'inventaire ansible-inventory -i $(FICHIER_INVENTAIRE) --graph inventaire-hote: ansible-runtime ## Affiche les variables derivees d'un hote — HOTE= @if [[ -z "$(HOTE)" ]]; then \ printf '%s\n' 'Refus: relancer avec HOTE=nom_hote.'; \ exit 2; \ fi ansible-inventory -i $(FICHIER_INVENTAIRE) --host $(HOTE) inventaire-lab: ## Affiche le graphe de l'inventaire de laboratoire $(MAKE) inventaire-graphe FICHIER_INVENTAIRE="$(INVENTAIRE_LAB)" inventaire-production: ## Affiche le graphe de l'inventaire de production $(MAKE) inventaire-graphe FICHIER_INVENTAIRE="$(INVENTAIRE_PRODUCTION)" .PHONY: _verifier-acces-modele _verifier-privileges-modele preparer-modele verifier-modele nettoyer-modele _verifier-acces-modele: ansible-runtime _modele-requis ansible -i $(INVENTAIRE_MODELE) $(CIBLE_MODELE) $(UTILISATEUR_MODELE) -m ping -e ansible_become=false _verifier-privileges-modele: ansible-runtime _modele-requis ansible -i $(INVENTAIRE_MODELE) $(CIBLE_MODELE) $(UTILISATEUR_MODELE) -b -m command -a "whoami" # Le gabarit dore n'est PAS un hote du tenant : sa configuration Proxmox le place sur le # reseau de fabrication (192.168.12.x), pas dans le supernet. `instancier` emet donc # `modeles_vm` toujours VIDE, et les etats d'un serveur ne connaissent que `actif` et # `planifie` — rien ne pouvait y entrer. Resultat mesure le 2026-08-09 : trois cibles # documentees qui ne pouvaient rien faire, et Ansible qui repondait « skipping: no hosts # matched » sans que ce soit une erreur. # # On DESIGNE donc la machine explicitement : `MODELE_HOTE=`. A defaut, on # refuse au lieu de ne rien faire — une commande qui ne fait rien en silence est pire # qu'une commande absente. , := , _modele-requis: @if [[ -z "$(MODELE_HOTE)" ]] && ! ansible-inventory -i $(INVENTAIRE_LAB) --list 2>/dev/null | python3 -c 'import json,sys; d=json.load(sys.stdin); sys.exit(0 if (d.get("modeles_vm") or {}).get("hosts") else 1)' 2>/dev/null; then \ printf '%s\n' 'Refus: aucune VM de gabarit designee.'; \ printf '%s\n' ' Le gabarit vit sur le reseau de fabrication, pas dans le tenant :'; \ printf '%s\n' ' il ne peut pas venir du plan. Le designer explicitement —'; \ printf '%s\n' ' make preparer-modele MODELE_HOTE=192.168.12.99'; \ printf '%s\n' ' make verifier-modele MODELE_HOTE=192.168.12.99'; \ printf '%s\n' ' make nettoyer-modele MODELE_HOTE=192.168.12.99 CONFIRMER=true'; \ printf '%s\n' ' (la VM doit etre DEMARREE : un template Proxmox ne boote pas ;'; \ printf '%s\n' ' le convertir en VM ou en cloner une copie de travail.)'; \ exit 2; \ fi # `-i ","` : la virgule finale fait de la chaine un inventaire d'un seul hote. INVENTAIRE_MODELE = $(if $(MODELE_HOTE),"$(MODELE_HOTE)$(,)",$(INVENTAIRE_LAB)) # Cible : la machine designee, ou le groupe si l'inventaire en contient un. CIBLE_MODELE = $(if $(MODELE_HOTE),$(MODELE_HOTE),$(GROUPE_MODELE)) # Un inventaire d'UN SEUL HOTE (`-i ","`) ne porte aucun `group_vars` : il faut donc # lui donner l'utilisateur de connexion, sinon Ansible tente le compte local de # l'operateur. Defaut `ansible`, qui est le `ciuser` du gabarit. MODELE_UTILISATEUR ?= ansible UTILISATEUR_MODELE = $(if $(MODELE_HOTE),-e ansible_user=$(MODELE_UTILISATEUR) -e cible_modele=$(MODELE_HOTE),) preparer-modele: ansible-runtime _modele-requis _verifier-acces-modele _verifier-privileges-modele ## Prepare le gabarit dore (VM de reference clonee pour chaque hote) ansible-playbook -i $(INVENTAIRE_MODELE) $(UTILISATEUR_MODELE) $(PLAYBOOK_PREPARER_MODELE) verifier-modele: ansible-runtime _modele-requis ## Verifie le gabarit dore ansible-playbook -i $(INVENTAIRE_MODELE) $(UTILISATEUR_MODELE) $(PLAYBOOK_VERIFIER_MODELE) nettoyer-modele: ansible-runtime _modele-requis ## Nettoie le gabarit avant capture — exige CONFIRMER=true @if [[ "$(CONFIRMER)" != "true" ]]; then \ printf '%s\n' 'Refus: relancer avec CONFIRMER=true pour le nettoyage final du modele.'; \ exit 2; \ fi ansible-playbook -i $(INVENTAIRE_MODELE) $(UTILISATEUR_MODELE) $(PLAYBOOK_NETTOYER_MODELE) -e template_cleanup_confirm=true .PHONY: _verifier-acces-hote _verifier-privileges-hote _attendre-hote faits verifier-hote # Deux ATTENTES ACTIVES, sans lesquelles on ne peut pas enchainer creation et # deploiement — donc sans lesquelles `make myDay` ne peut pas reconstruire seul. # # 1. SSH. `make creer-vm` rend la main des que Proxmox a DEMARRE la VM, pas quand elle # repond. Verifier l'acces aussitot echoue presque toujours sur une machine neuve — # et le symptome trompe : a travers la frontiere, le TCP s'etablit (SYN proxy) et # l'echec se lit « Connection timed out during banner exchange ». # # 2. Le verrou dpkg. L'image Debian lance ses propres mises a jour au premier # demarrage et tient `/var/lib/dpkg/lock-frontend` plusieurs minutes. Le premier # `apt` d'Ansible echoue alors sur un verrou, pas sur une vraie erreur. # # `unattended-upgrades.service` est volontairement ABSENT de la condition : c'est un # DEMON (Type=simple), toujours `active`. L'y inclure rendait l'attente impossible a # satisfaire — elle echouait au bout du delai, systematiquement. Seules `apt-daily*` # sont des one-shot, et c'est le VERROU qui dit si dpkg est reellement occupe. # # Les deux sont des COURSES de premier demarrage, pas des defauts de conception. On les # attend au lieu de les subir. ATTENTE_HOTE (secondes) borne chacune : depasser le # delai reste un echec, pour ne pas transformer une panne en attente infinie. ATTENTE_HOTE ?= 600 _attendre-hote: ansible-runtime @set -e; \ if [[ -z "$(LIMITE)" ]]; then printf '%s\n' 'Refus: LIMITE requis.'; exit 2; fi; \ fin=$$(( SECONDS + $(ATTENTE_HOTE) )); \ printf '%s' "Attente de SSH sur $(LIMITE) "; \ until ansible -i $(INVENTAIRE_PRODUCTION) $(LIMITE) -m ping -e ansible_become=false >/dev/null 2>&1; do \ if (( SECONDS > fin )); then printf '%s\n' " ECHEC: injoignable apres $(ATTENTE_HOTE)s."; exit 4; fi; \ printf '.'; sleep 5; \ done; \ printf '%s\n' " ok"; \ printf '%s' "Attente de cloud-init et des maj automatiques "; \ until ansible -i $(INVENTAIRE_PRODUCTION) $(LIMITE) -b -m shell -a \ 'cloud-init status --wait >/dev/null 2>&1; \ ! systemctl is-active --quiet apt-daily.service apt-daily-upgrade.service \ && ! fuser /var/lib/dpkg/lock-frontend >/dev/null 2>&1' >/dev/null 2>&1; do \ if (( SECONDS > fin )); then printf '%s\n' " ECHEC: dpkg toujours occupe apres $(ATTENTE_HOTE)s."; exit 4; fi; \ printf '.'; sleep 10; \ done; \ printf '%s\n' " libere" _verifier-acces-hote: ansible-runtime ansible -i $(INVENTAIRE_PRODUCTION) $(LIMITE) -m ping -e ansible_become=false _verifier-privileges-hote: ansible-runtime ansible -i $(INVENTAIRE_PRODUCTION) $(LIMITE) -b -m command -a "whoami" faits: ansible-runtime ## Interroge les faits Ansible de la flotte — LIMITE= ansible -i $(INVENTAIRE_PRODUCTION) $(LIMITE) -m setup -a "filter=ansible_distribution*" verifier-hote: ansible-runtime ## Passe le playbook de verification sur un hote ansible-playbook -i $(INVENTAIRE_PRODUCTION) $(PLAYBOOK_VERIFIER_HOTE) $(OPTIONS_PLAYBOOK)