Set-OPS-Public/Makefile
Daniel Allaire 1b71884871 make fiches : les 68, et la cible qui les regenere
Une preuve a mordu : un script qu aucune cible n appelle est du code mort. Elle
avait raison.

68 fiches generees. Le vide y est une information — la carte de ce qui reste a
faire, tenue a jour toute seule.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-14 13:14:01 -04:00

1645 lines
87 KiB
Makefile

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-<depot-en-minuscules>`.
#
# UN `ANSIBLE_VAULT_PASSWORD_FILE` NE PEUT PLUS SUFFIRE : `cloner_vm_debian.yml` charge
# la voute du TENANT puis celle de l'UNDERLAY dans la meme execution — les identifiants
# Proxmox appartiennent a l'hebergeur, le reste au locataire. `ANSIBLE_VAULT_IDENTITY_LIST`
# porte plusieurs secrets et les essaie tous ; un seul export suffit donc pour les 28
# appels a `ansible-playbook`, sans en toucher un seul.
#
# `?=` : une valeur deja posee dans l'environnement gagne — la GUI et les runners peuvent
# nommer leurs propres cles sans que ce fichier ait a les connaitre.
export ANSIBLE_VAULT_IDENTITY_LIST ?= $(shell python3 scripts/voutes.py identites 2>/dev/null)
# Inventaire de l'instance : un seul par instance dans le modèle « séparation par
# instance ». Détection rétro-compatible : principal > production > lab.
# Surchargeable : make … SETOPS_INVENTAIRE=chemin/hosts.yml
#
# 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/<modele> ../mon-instance && ln -s ../mon-instance instance"; \
printf '%s\n' " modeles disponibles : $$(ls exemples/modeles 2>/dev/null | grep -v '\.md' | tr '\n' ' ')"; \
exit 2; \
fi
.PHONY: aide
aide: ## Affiche l'aide detaillee du moteur (au-dela de cette liste)
@printf '%s\n' 'Set-OPS — moteur d ecosystemes numeriques souverains'
@printf '%s\n' ''
@printf '%s\n' 'Nouveau ? -> QUICKSTART.md (de zero a ton ecosysteme sur Proxmox)'
@printf '%s\n' 'Flux: editer le plan -> make instancier -> make instancier-appliquer -> make deployer'
@printf '%s\n' ''
@printf '%s\n' 'VM'
@printf '%s\n' ' Creer une VM (VMID/IP/VLAN/passerelle lus dans le plan):'
@printf '%s\n' ' make creer-vm HOTE=web-frontal-01'
@printf '%s\n' ' Cloner seulement, sans passer par le plan:'
@printf '%s\n' ' make cloner-vm HOTE=web-frontal-01 VMID=95301 VLAN=15 ADRESSE_IP=10.0.2.31 PASSERELLE=10.0.2.1'
@printf '%s\n' ' Configurer Proxmox et le Vault API (parametres: docs/config-proxmox.md):'
@printf '%s\n' ' make config'
@printf '%s\n' ''
@printf '%s\n' 'Hotes'
@printf '%s\n' ' Planifier/modifier un hote: editer le plan, puis regenerer:'
@printf '%s\n' ' editer instance/plan/serveurs.yml (ou la vue Serveurs du GUI)'
@printf '%s\n' ' make instancier-appliquer'
@printf '%s\n' ' Verifier un deploiement a blanc (dry-run):'
@printf '%s\n' ' make verifier-deploiement HOTE=web-frontal-01'
@printf '%s\n' ' Remettre un hote en conformite selon ses groupes:'
@printf '%s\n' ' make deployer HOTE=web-frontal-01'
@printf '%s\n' ' Afficher un hote:'
@printf '%s\n' ' make hote-afficher HOTE=web-frontal-01'
@printf '%s\n' ' Diagnostiquer:'
@printf '%s\n' ' make verifier-hote LIMITE=web-frontal-01'
@printf '%s\n' ''
@printf '%s\n' 'Groupes'
@printf '%s\n' ' Appliquer un groupe complet:'
@printf '%s\n' ' make deployer-groupe GROUPE=serveur_debian'
@printf '%s\n' ' Convention:'
@printf '%s\n' ' groupe serveur_debian -> playbooks/groupes/serveur_debian.yml'
@printf '%s\n' ''
@printf '%s\n' 'Ecosysteme complet (orchestrateur)'
@printf '%s\n' ' Ordre de deploiement (couches + graphe):'
@printf '%s\n' ' make site-verifier # valide la coherence couches/graphe'
@printf '%s\n' ' python3 scripts/orchestrer.py ordre'
@printf '%s\n' ' (Re)generer playbooks/site.yml ordonne:'
@printf '%s\n' ' make site'
@printf '%s\n' ' CONFIGURER la flotte existante, couche par couche (2b, VM deja creees):'
@printf '%s\n' ' make deployer-tout CONFIRMER=true # (MODE_CHECK=1 pour un essai a blanc idempotent)'
@printf '%s\n' ' CREER toutes les VM du plan (2a, clone Proxmox):'
@printf '%s\n' ' make flotte-creer CONFIRMER=true'
@printf '%s\n' ' RECONSTRUIRE from-zero = creer les VM PUIS deployer (2a+2b, VM inexistantes):'
@printf '%s\n' ' make reconstruire CONFIRMER=true # alias: make myDay CONFIRMER=true'
@printf '%s\n' ''
@printf '%s\n' 'Flux reseau (pare-feu / audit)'
@printf '%s\n' ' Matrice d audit + apercus nftables resolus (NON actives):'
@printf '%s\n' ' make flux # -> docs/registre-flux.md + instance/flux-genere/*.nft'
@printf '%s\n' ' make flux-verifier # valide schema + coherence de matrice'
@printf '%s\n' ''
@printf '%s\n' 'Wiki pedagogique'
@printf '%s\n' ' Publier wiki/ dans le wiki Forgejo (source versionnee -> vue browsable):'
@printf '%s\n' ' make wiki-publier WIKI_REMOTE=https://forge.<domaine>/<proprio>/<depot>.wiki.git'
@printf '%s\n' ''
@printf '%s\n' 'Inventaires'
@printf '%s\n' ' Graphe de production:'
@printf '%s\n' ' make inventaire'
@printf '%s\n' ' Graphe explicite:'
@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=<dossier> [HOTE=<nom>]
@set -e; \
if [[ -z "$(TENANT)" ]]; then \
printf '%s\n' 'Refus: relancer avec TENANT=<dossier frere>, ex. TENANT=OPS-Chezlepro.'; \
exit 2; \
fi; \
if [[ ! -d "../$(TENANT)/plan" ]]; then \
printf '%s\n' 'Refus: ../$(TENANT)/plan introuvable — ce n est pas un ecosysteme.'; \
exit 2; \
fi; \
inv="$$(env -u SETOPS_INVENTAIRE SETOPS_INSTANCE=../$(TENANT) python3 -c 'import sys; sys.path.insert(0,"scripts"); from inventory_rules import inventaire_de; print(inventaire_de() or "")')"; \
if [[ -z "$$inv" || ! -f "$$inv" ]]; then \
printf '%s\n' "Refus: aucun inventaire pour $(TENANT). Le generer chez lui (make instancier)."; \
exit 2; \
fi; \
hote="$(HOTE)"; \
if [[ -z "$$hote" ]]; then \
hote="$$(python3 -c 'import sys,yaml; d=yaml.safe_load(open(sys.argv[1])) or {}; p=[d.get("all",{})]; out=[]; \
[ (out.extend((n.get("children") or {}).get("serveur_ops_tenant",{}).get("hosts",{}) or {}), p.extend((n.get("children") or {}).values())) for n in iter(lambda: p.pop() if p else None, None) ]; print(" ".join(sorted(set(out))))' "$$inv")"; \
fi; \
if [[ -z "$$hote" ]]; then \
printf '%s\n' "Refus: $(TENANT) ne declare aucun hote portant serveur_ops_tenant."; \
printf '%s\n' "L insemination vise LE RUNNER d un tenant, jamais une machine ordinaire."; \
exit 2; \
fi; \
if (( $$(wc -w <<< "$$hote") > 1 )); then \
printf '%s\n' "Refus: plusieurs runners declares ($$hote). Nommer lequel avec HOTE=."; \
exit 2; \
fi; \
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=<artefacts|genome|resolveur> [HOTE=] CONFIRMER=true
@set -e; \
if [[ -z "$(SERVICE)" ]]; then \
printf '%s\n' 'Refus: relancer avec SERVICE=<artefacts|genome|resolveur>.'; \
exit 2; \
fi; \
if [[ "$(CONFIRMER)" != "true" ]]; then \
printf '%s\n' 'Refus: cette preuve COUPE l amont quelques secondes pour mesurer.'; \
printf '%s\n' 'La coupure est retiree quoi qu il arrive (bloc `always`), mais'; \
printf '%s\n' 'pendant ce temps la fonction eprouvee peut echouer sur cet hote.'; \
printf '%s\n' 'Relancer avec CONFIRMER=true.'; \
exit 2; \
fi; \
ansible-playbook -i $(INVENTAIRE_PRODUCTION) playbooks/maintenance/emancipation-prouver.yml \
-e emancipation_service=$(SERVICE) \
$(if $(HOTE),-e emancipation_hotes=$(HOTE)) $(ARGS)
.PHONY: sonder
sonder: ## Sonde une cible et DIT ce qui distingue — CIBLE=<hote|ip> [PORTS="22 443"]
@if [[ -z "$(CIBLE)" ]]; then \
printf '%s\n' 'Refus: relancer avec CIBLE=<hote du plan, ou adresse>.'; \
printf '%s\n' 'Ex. make sonder CIBLE=infra-dns-01 PORTS="53 22"'; \
exit 2; \
fi
@python3 scripts/sonder.py $(CIBLE) $(or $(PORTS),22) $(if $(DELAI),--delai $(DELAI))
.PHONY: verifier
verifier: lint test inventaire-verifier site-verifier flux-verifier syntaxe ## Rejoue les preuves SANS reecrire le rapport (verification rapide)
python3 scripts/prouver.py --verifier
# 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-<date>.md (piece justificative horodatee, rejouable).
# `make verifier` l'appelle en mode --verifier (preuves seules, aucun rapport ecrit).
.PHONY: prouver
prouver: ansible-runtime _instance-requise ## Execute les preuves et ecrit docs/audit/preuve-<date>.md
python3 scripts/prouver.py
.PHONY: inventaire hote-planifier hote-ajouter hote-groupes hote-afficher appliquer deployer deployer-groupe cloner-vm creer-vm config inventaire-ui inventaire-verifier inventaire-lister inventaire-graphe inventaire-hote inventaire-lab inventaire-production instance-utiliser instance-courante
inventaire: inventaire-production ## Verifie l'inventaire et en affiche le graphe (lab puis production)
# Bascule le symlink 'instance' vers un autre dépôt d'instance (séparation par
# instance : prod vs bac à sable). Ex. : make instance-utiliser NOM=OPS-Chezlepro-lab
instance-utiliser: ## Bascule l'instance active (symlink instance/) vers un dossier frere — NOM=<dossier>
@if [[ -z "$(NOM)" ]]; then printf '%s\n' "Usage: make instance-utiliser NOM=<dossier-frère> (ex. OPS-Chezlepro-lab)"; exit 2; fi
@if [[ ! -d "../$(NOM)" ]]; then printf '%s\n' "Introuvable: ../$(NOM)"; exit 2; fi
@if [[ -e instance && ! -L instance ]]; then printf '%s\n' "Refus: 'instance' existe et n'est pas un symlink."; exit 2; fi
@rm -f instance && ln -s "../$(NOM)" instance
@printf 'instance -> %s\n' "$$(readlink instance)"
instance-courante: ## Affiche vers quel ecosysteme pointe l'instance active
@printf 'instance -> %s\n' "$$(readlink instance 2>/dev/null || echo '(non monté)')"
# Vue d'ensemble : toutes les instances de la fédération, l'active (*), leur index,
# plage VLAN, statut fédéré/prod ; signale les collisions d'index. Lecture seule.
instances: ## Liste les ecosystemes decouverts (dossiers freres) et signale les collisions d'index
@python3 scripts/instances.py
# (Re)génère le plan de recette (docs/audit/plan-de-recette.md) depuis les exercices
# du wiki. La preuve P22 vérifie qu'il reste à jour.
plan-recette: ## Regenere docs/audit/plan-de-recette.md depuis le wiki
@python3 scripts/plan_recette.py
# Liste les modèles disponibles (socle + SETOPS_MODELES) pour créer une instance.
instance-modeles: ## Liste les modeles d'ecosysteme disponibles
@python3 scripts/instance_creer.py --lister-modeles
# Crée un dépôt d'instance frère depuis un modèle. Ne bascule pas le symlink.
# Ex. : make instance-creer NOM=OPS-ClientX MODELE=socle INDEX=4
instance-creer: ## Cree un nouvel ecosysteme depuis un modele — NOM=<nom> MODELE=<modele>
@python3 scripts/instance_creer.py --nom "$(NOM)" --modele "$(MODELE)" \
$(if $(INDEX),--index $(INDEX),)
# Crée un MODÈLE (dépôt privé). Deux modes :
# base : copier un modèle générique -> make model-creer MODE=base BASE=identite NOM=maison-obnl
# instance : promouvoir une instance -> make model-creer MODE=instance SOURCE=OPS-Chezlepro NOM=cabinet
model-creer: ## Cree un modele d'ecosysteme — MODE=<mode> NOM=<nom>
@python3 scripts/model_creer.py --mode "$(MODE)" --nom "$(NOM)" \
$(if $(BASE),--base $(BASE),) $(if $(SOURCE),--source $(SOURCE),) $(if $(DEST),--dest $(DEST),)
config: ## Affiche la configuration Proxmox lue par le moteur
python3 scripts/config_proxmox.py
inventaire-ui: _instance-requise ## Ouvre la console d'exploitation (GUI web) sur l'inventaire actif
python3 scripts/inventory_gui.py --inventaire $(FICHIER_INVENTAIRE)
hote-ajouter hote-planifier hote-groupes:
@printf '%s\n' 'Cible depreciee: l inventaire est GENERE depuis le plan, il ne s edite plus a la main.'
@printf '%s\n' 'Declare ou modifie l hote dans instance/plan/serveurs.yml (ou la vue Serveurs du GUI), puis :'
@printf '%s\n' ' make instancier-appliquer'
@printf '%s\n' '(creer-vm lit desormais VMID/IP/VLAN/passerelle directement dans l inventaire genere.)'
@exit 2
hote-afficher: ansible-runtime ## Affiche tout ce que le plan derive pour un hote — HOTE=<nom>
@if [[ -z "$(HOTE)" ]]; then \
printf '%s\n' 'Refus: relancer avec HOTE=nom_hote.'; \
exit 2; \
fi
python3 scripts/inventory_host.py --inventaire $(FICHIER_INVENTAIRE) afficher --hote $(HOTE)
appliquer: ansible-runtime ## Applique un groupe a la flotte — GROUPE=<groupe>
@if [[ -z "$(GROUPE)" ]]; then \
printf '%s\n' 'Refus: relancer avec GROUPE=nom_groupe.'; \
exit 2; \
fi
@if [[ ! -f "$(DOSSIER_PLAYBOOKS_GROUPES)/$(GROUPE).yml" ]]; then \
printf '%s\n' 'Refus: aucun playbook pour ce groupe: $(DOSSIER_PLAYBOOKS_GROUPES)/$(GROUPE).yml'; \
exit 2; \
fi
python3 scripts/inventory_host.py --inventaire $(INVENTAIRE_PRODUCTION) --dependances $(FICHIER_DEPENDANCES) verifier-dependances-groupe --groupe $(GROUPE)
ansible-playbook -i $(INVENTAIRE_PRODUCTION) "$(DOSSIER_PLAYBOOKS_GROUPES)/$(GROUPE).yml" --limit '$(GROUPE):&$(GROUPE_HOTES_ACTIFS)'
# ATTENDRE PLUTOT QUE CONSTATER (2026-08-27). `deployer` faisait un `ping` unique
# (`_verifier-acces-hote`). Depuis que `creer-vm` ne guette plus le SSH — il prouve la
# materialisation par l'agent invite, seul geste que le runner du SITE ait le droit de
# faire chez un tenant — c'est ICI qu'il faut attendre : enchainer `creer-vm` puis
# `deployer` tomberait sinon sur une machine encore en cloud-init. On y gagne au passage
# ce que le `ping` unique ne faisait pas : attendre que `dpkg` se libere, faute de quoi
# la premiere couche echoue sur un verrou APT.
#
# CONTREPARTIE ASSUMEE : un nom d'hote errone ne fait plus echouer tout de suite, il
# patiente ATTENTE_HOTE secondes. Echouer vite sur l'injoignable interdirait de chainer
# la creation au deploiement — le geste central d'une reconstruction.
deployer: _instance-requise ## Deploie un hote, couche par couche, dans l'ordre du graphe — HOTE=<nom>
@set -e; \
if [[ -z "$(HOTE)" ]]; then \
printf '%s\n' 'Refus: relancer avec HOTE=nom_hote.'; \
exit 2; \
fi; \
python3 scripts/inventory_host.py --inventaire $(FICHIER_INVENTAIRE) verifier-actif --hote $(HOTE); \
python3 scripts/inventory_host.py --inventaire $(FICHIER_INVENTAIRE) --dependances $(FICHIER_DEPENDANCES) verifier-dependances-hote --hote $(HOTE); \
playbooks="$$(python3 scripts/inventory_host.py --inventaire $(FICHIER_INVENTAIRE) playbooks --hote $(HOTE) --dossier-playbooks $(DOSSIER_PLAYBOOKS_GROUPES))"; \
if [[ -z "$$playbooks" ]]; then \
printf '%s\n' 'Refus: aucun playbook applicable pour HOTE=$(HOTE).'; \
exit 2; \
fi; \
$(VAULT_UNE_FOIS) \
$(MAKE) --no-print-directory _attendre-hote LIMITE="$(HOTE)"; \
$(MAKE) _verifier-privileges-hote LIMITE="$(HOTE)"; \
for playbook in $$playbooks; do \
ansible-playbook -i $(INVENTAIRE_PRODUCTION) "$$playbook" --limit "$(HOTE)"; \
done; \
$(MAKE) verifier-hote LIMITE="$(HOTE)"
.PHONY: site site-verifier deployer-tout
site: ansible-runtime ## Regenere playbooks/site.yml depuis les couches et le graphe de dependances
python3 scripts/orchestrer.py ecrire
ansible-playbook -i $(INVENTAIRE_PRODUCTION) playbooks/site.yml --syntax-check
site-verifier: ## Verifie que playbooks/site.yml correspond aux couches declarees
python3 scripts/orchestrer.py verifier
.PHONY: flux flux-verifier
cles-recenser: ## Montre ce qui n'existe QUE sur ce poste (sans rien ecrire)
python3 scripts/exporter_cles.py --recenser
cles-exporter: ## Sort les cles du poste, chiffrees et relues — VERS=<repertoire>
@# A LANCER SOI-MEME, PAS PAR UN AGENT. `gpg` demande une phrase de passe : elle ne
@# doit passer ni par un journal, ni par le contexte d'un assistant. Taper la commande
@# dans son propre terminal est la seule facon de s'en assurer.
@if [ -z "$(VERS)" ]; then \
printf 'Refus: relancer avec VERS=<repertoire de destination>.\n'; \
printf 'Ex. make cles-exporter VERS=/media/danallaire/CLE\n'; exit 2; fi
python3 scripts/exporter_cles.py --vers "$(VERS)"
.PHONY: cles-recenser cles-exporter
depot-hors-site: ## Copie le depot de sauvegarde du site HORS du site — VERS=<repertoire>
@# Le depot heberge la racine de l'AC, la forge du genome, et l'etat de CHAQUE
@# locataire — sur une seule machine, dans un seul batiment. La copie est opaque
@# (chiffree cote client) : elle se depose chez un pair sans rien lui demander
@# d'autre que de la disponibilite.
@if [ -z "$(VERS)" ]; then \
printf 'Refus: relancer avec VERS=<repertoire de destination>.\n'; \
printf 'Ex. make depot-hors-site VERS=/media/danallaire/setops-coffre\n'; exit 2; fi
@# EN JSON, PAS EN `-e cle=valeur` (2026-09-05). La forme `cle=valeur` DECOUPE SUR
@# LES BLANCS — c'est ainsi qu'on passe plusieurs variables d'un coup. Un chemin
@# contenant une espace y perd donc tout ce qui suit le premier blanc, et l'erreur
@# parle d'un repertoire introuvable au nom tronque : « /media/danallaire/Cle ».
@# Le depot avait deja appris ca pour le clonage de VM ; la lecon ne s'etait pas
@# propagee ici. Quatrieme fois que ce piege est paye.
ansible-playbook -i scripts/site_inventaire.py \
playbooks/maintenance/depot-hors-site.yml -e '{"vers": "$(VERS)"}'
.PHONY: depot-hors-site
cles-compagnons: ## Depose le script de restauration sur une cle DEJA faite — VERS=<repertoire>
@# POUR UNE CLE EXISTANTE. L'archive n'est pas refaite : ni phrase de passe redemandee,
@# ni risque d'ecraser ce qui est deja verifie.
@if [ -z "$(VERS)" ]; then \
printf 'Refus: relancer avec VERS=<repertoire de la cle>.\n'; exit 2; fi
python3 scripts/exporter_cles.py --vers "$(VERS)" --compagnons
cles-restaurer: ## Remet les cles en place depuis une archive — ARCHIVE=<fichier>
@# EN VRAI, ON N'UTILISE PAS CETTE CIBLE. Le jour ou l'on restaure, le depot n'est pas
@# la — le cloner demande la cle SSH qu'on essaie justement de recuperer. On lance
@# alors `restaurer_cles.py` DEPUIS LA CLE USB, ou les trois commandes du LISEZ-MOI.
@# Cette cible existe pour eprouver la manoeuvre pendant qu'on a encore le choix.
@if [ -z "$(ARCHIVE)" ]; then \
printf 'Refus: relancer avec ARCHIVE=<chemin de l archive .tar.gpg>.\n'; exit 2; fi
python3 scripts/restaurer_cles.py "$(ARCHIVE)" $(if $(FORCER),--forcer)
.PHONY: cles-compagnons cles-restaurer
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
@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; \
: ' UN WIKI VIDE SE CLONE TRES BIEN, et le garde-fou ci-dessus ne le voyait pas :'; \
: ' il ne teste que l ECHEC du clone, alors que son message parle du cas VIDE.'; \
: ' Consequence mesuree le 2026-09-08 contre un depot bare local : la recette'; \
: ' allait au bout et creait une branche `master` — parce que `init.defaultBranch`'; \
: ' vaut master sur ce poste — alors que le wiki deja publie vit sur `main`. Le nom'; \
: ' de branche etait DEVINE, et il divergeait d une forge a l autre.'; \
: ' On refuse donc plutot que de choisir a la place de Forgejo : creer une premiere'; \
: ' page dans son interface initialise le wiki avec LA branche qu il attend.'; \
: ' ON NE DEVINE PAS — ON DEMANDE. Forgejo DECLARE la branche qu il attend, dans'; \
: ' `wiki_branch` de son API : GET /api/v1/repos/<proprio>/<depot>. Mesure du'; \
: ' 2026-09-09 sur forge.genese.internal : `wiki_branch = main`. Le refus ci-dessous'; \
: ' reste le DEFAUT — depuis ce poste l API n est pas joignable (la forge du site'; \
: ' n accepte le 443 que des machines qui declarent le flux) — mais qui a la reponse'; \
: ' peut la donner, au lieu de passer par l interface web :'; \
: ' make wiki-publier WIKI_REMOTE=... WIKI_BRANCHE=main'; \
: ' C est la difference entre une garde qui protege et une garde qui bloque.'; \
if ! git -C "$$tmp/wiki" rev-parse --verify --quiet HEAD >/dev/null; then \
if [[ -n "$(WIKI_BRANCHE)" ]]; then \
printf '%s\n' "Wiki vide : initialisation sur la branche declaree « $(WIKI_BRANCHE) »."; \
git -C "$$tmp/wiki" checkout -q -b "$(WIKI_BRANCHE)"; \
else \
printf '%s\n' 'Refus: ce wiki est VIDE (aucun commit).' \
'Publier ici inventerait un nom de branche — sur ce poste `master`, alors que' \
'le wiki deja en service vit sur `main`. Deux issues :' \
' - demander a Forgejo : GET /api/v1/repos/<proprio>/<depot> -> `wiki_branch`,' \
' puis make wiki-publier WIKI_REMOTE=... WIKI_BRANCHE=<cette valeur>' \
" - ou creer une premiere page dans l interface Forgejo de $(WIKI_REMOTE)"; \
exit 2; \
fi; \
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: $(WIKI_REMOTE)" \
"source: $$sha" \
"date: $$(date +%F)"; } > "$(CURDIR)/docs/audit/wiki-publie.yml"; \
printf '%s\n' 'Temoin depose : docs/audit/wiki-publie.yml (a committer).'
deployer-tout: _instance-requise ## Deploie TOUTE la flotte dans l'ordre des couches
@set -e; \
if [[ "$(CONFIRMER)" != "true" ]]; then \
printf '%s\n' 'Refus: deploiement ORCHESTRE de TOUTE la flotte (action impactante).'; \
printf '%s\n' 'Relancer avec CONFIRMER=true. Astuce: tester d abord en idempotent avec MODE_CHECK=1.'; \
exit 2; \
fi; \
python3 scripts/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: <hote> ===` en direct pour suivre l'avancement,
# et le detail arrive ordonne.
#
# Un echec n'est pas avale : le code de retour de chaque hote est relu, et la cible sort
# en erreur si l'un d'eux a echoue — sinon `_attendre-flotte` partirait sur une flotte
# incomplete.
# --- LE MOT DE PASSE DE LA VOUTE, DEMANDE UNE SEULE FOIS ----------------------
#
# Deux cibles longues en ont besoin : `deployer-tout` (une invocation ansible) et
# `flotte-creer` (une invocation PAR MACHINE). La seconde redemandait le mot de passe a
# chaque VM — quinze fois sur la reconstruction de Chezlepro, quatre en parallele, avec
# la sortie redirigee vers des journaux : l'exploitant est harcele par des invites qu'il
# ne voit meme pas.
#
# UN SEUL BLOC POUR LES DEUX. Deux facons de manipuler un secret finiraient par diverger,
# et c'est le genre de divergence qu'on ne remarque qu'apres.
#
# `-t 0` : ne demander que si un humain est au clavier. Un appel non interactif refuse
# clairement plutot que de pendre sur une invite que personne ne lit.
define VAULT_UNE_FOIS
vault_chiffre="$$(grep -rlsIF '$$ANSIBLE_VAULT' $(dir $(INVENTAIRE_PRODUCTION))group_vars 2>/dev/null | head -1 || true)"; \
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=<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.
@#
@# 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=<nom> 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=<un seul>`.
@#
@# 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=<nom> pour un seul
@set -e; \
voute="$$(dirname "$$(readlink -f underlay.yml)")/underlay.vault.yml"; \
supp=(); [[ -f "$$voute" ]] && supp+=( -e "@$$voute" ); \
SETOPS_DEPOT="$(DEPOT)" ansible-playbook -i scripts/site_inventaire.py \
playbooks/maintenance/genome_pousser.yml "$${supp[@]}" $(ARGS)
.PHONY: site-creer
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=<depot>
@# CETTE CIBLE MANQUAIT, ET SON ABSENCE AVAIT UN NOM (2026-09-12). `raser` ne vise que
@# l'instance ACTIVE — un locataire. Rien ne detruisait les machines du site : ce n'est
@# donc pas que personne n'avait essaye de le reconstruire depuis zero, c'est que
@# l'outil n'en offrait pas le moyen. La limite « le site n'a jamais ete reconstruit »
@# se lisait partout sans que sa CAUSE soit nommee.
@#
@# LE SITE PORTE LE DEPOT DE SAUVEGARDE DE CHAQUE LOCATAIRE et la racine de son AC.
@# `make depot-hors-site VERS=<repertoire>` AVANT, toujours.
python3 scripts/site_raser.py $(if $(filter true,$(CONFIRMER)),--confirmer) \
$(if $(SITE),--site "$(SITE)") $(if $(MACHINE),--machine "$(MACHINE)")
.PHONY: site-raser
site-creer: ## Cree les machines du site depuis l'underlay — CONFIRMER=true
@set -e; \
if [[ "$(CONFIRMER)" != "true" ]]; then \
printf '%s\n' 'Refus: creation des VM de l hebergeur (clone Proxmox).'; \
printf '%s\n' 'Relancer avec CONFIRMER=true.'; \
exit 2; \
fi; \
machines="$$(python3 scripts/site_machines.py lister)"; \
if [[ -z "$$machines" ]]; then printf '%s\n' 'Refus: aucune machine declaree dans l underlay.'; exit 2; fi; \
for m in $$machines; do \
printf '\n=== Machine du site: %s ===\n' "$$m"; \
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=<nom>
@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=<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=<nom> VMID=<id>
@if [[ -z "$(HOTE)" || -z "$(VMID)" ]]; then \
printf '%s\n' 'Refus: relancer avec HOTE=nom VMID=id_clone.'; \
exit 2; \
fi
@# En SDN EVPN, l'etiquette est portee par le VNet : `instancier` emet donc un VLAN
@# VIDE et un pont derive (t17serv). Exiger un VLAN ici rejetait toute VM de tenant.
@# Un VLAN vide n'est accepte QUE si un pont est fourni — sinon la VM n'aurait ni
@# etiquette ni VNet, et se retrouverait branchee nulle part.
@if [[ -z "$(VLAN)" && -z "$(PONT_PROXMOX)" ]]; then \
printf '%s\n' 'Refus: relancer avec VLAN=id_vlan, ou PONT_PROXMOX=<vnet> en SDN.'; \
exit 2; \
fi
@if [[ -n "$(VLAN)" ]] && { ! [[ "$(VLAN)" =~ ^[0-9]+$$ ]] || (( 10#$(VLAN) < 1 || 10#$(VLAN) > 4094 )); }; then \
printf '%s\n' 'Refus: VLAN doit etre un nombre entre 1 et 4094.'; \
exit 2; \
fi
@if [[ -n "$(CLE_SSH_PUBLIQUE)" && ! -f "$(CLE_SSH_PUBLIQUE)" ]]; then \
printf '%s\n' 'Refus: cle publique SSH introuvable: $(CLE_SSH_PUBLIQUE)'; \
exit 2; \
fi
@if [[ "$(DHCP)" != "true" && ( -z "$(ADRESSE_IP)" || -z "$(CIDR)" || -z "$(PASSERELLE)" ) ]]; then \
printf '%s\n' 'Refus: fournir ADRESSE_IP, CIDR et PASSERELLE, ou utiliser DHCP=true.'; \
exit 2; \
fi
@ipconfig='ip=dhcp'; \
if [[ "$(DHCP)" != "true" ]]; then \
ipconfig='ip=$(ADRESSE_IP)/$(CIDR),gw=$(PASSERELLE)'; \
fi; \
extra_vars=( \
-e proxmox_clone_nom="$(HOTE)" \
-e proxmox_clone_vmid="$(VMID)" \
-e proxmox_clone_ipconfig0="$$ipconfig" \
-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=<nom>
@set -e; \
if [[ -z "$(HOTE)" ]]; then \
printf '%s\n' 'Refus: relancer avec HOTE=nom_hote (declare dans le plan).'; \
exit 2; \
fi; \
params="$$(python3 scripts/inventory_host.py --inventaire $(INVENTAIRE_PRODUCTION) parametres-proxmox --hote $(HOTE))"; \
eval "$$params"; \
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=<nom>
@if [[ -z "$(HOTE)" ]]; then \
printf '%s\n' 'Refus: relancer avec HOTE=nom_hote.'; \
exit 2; \
fi
ansible-inventory -i $(FICHIER_INVENTAIRE) --host $(HOTE)
inventaire-lab: ## Affiche le graphe de l'inventaire de laboratoire
$(MAKE) inventaire-graphe FICHIER_INVENTAIRE="$(INVENTAIRE_LAB)"
inventaire-production: ## Affiche le graphe de l'inventaire de production
$(MAKE) inventaire-graphe FICHIER_INVENTAIRE="$(INVENTAIRE_PRODUCTION)"
.PHONY: _verifier-acces-modele _verifier-privileges-modele preparer-modele verifier-modele nettoyer-modele
_verifier-acces-modele: ansible-runtime _modele-requis
ansible -i $(INVENTAIRE_MODELE) $(CIBLE_MODELE) $(UTILISATEUR_MODELE) -m ping -e ansible_become=false
_verifier-privileges-modele: ansible-runtime _modele-requis
ansible -i $(INVENTAIRE_MODELE) $(CIBLE_MODELE) $(UTILISATEUR_MODELE) -b -m command -a "whoami"
# Le gabarit dore n'est PAS un hote du tenant : sa configuration Proxmox le place sur le
# reseau de fabrication (192.168.12.x), pas dans le supernet. `instancier` emet donc
# `modeles_vm` toujours VIDE, et les etats d'un serveur ne connaissent que `actif` et
# `planifie` — rien ne pouvait y entrer. Resultat mesure le 2026-08-09 : trois cibles
# documentees qui ne pouvaient rien faire, et Ansible qui repondait « skipping: no hosts
# matched » sans que ce soit une erreur.
#
# On DESIGNE donc la machine explicitement : `MODELE_HOTE=<ip-ou-nom>`. A defaut, on
# refuse au lieu de ne rien faire — une commande qui ne fait rien en silence est pire
# qu'une commande absente.
, := ,
_modele-requis:
@if [[ -z "$(MODELE_HOTE)" ]] && ! ansible-inventory -i $(INVENTAIRE_LAB) --list 2>/dev/null | python3 -c 'import json,sys; d=json.load(sys.stdin); sys.exit(0 if (d.get("modeles_vm") or {}).get("hosts") else 1)' 2>/dev/null; then \
printf '%s\n' 'Refus: aucune VM de gabarit designee.'; \
printf '%s\n' ' Le gabarit vit sur le reseau de fabrication, pas dans le tenant :'; \
printf '%s\n' ' il ne peut pas venir du plan. Le designer explicitement —'; \
printf '%s\n' ' make preparer-modele MODELE_HOTE=192.168.12.99'; \
printf '%s\n' ' make verifier-modele MODELE_HOTE=192.168.12.99'; \
printf '%s\n' ' make nettoyer-modele MODELE_HOTE=192.168.12.99 CONFIRMER=true'; \
printf '%s\n' ' (la VM doit etre DEMARREE : un template Proxmox ne boote pas ;'; \
printf '%s\n' ' le convertir en VM ou en cloner une copie de travail.)'; \
exit 2; \
fi
# `-i "<hote>,"` : la virgule finale fait de la chaine un inventaire d'un seul hote.
INVENTAIRE_MODELE = $(if $(MODELE_HOTE),"$(MODELE_HOTE)$(,)",$(INVENTAIRE_LAB))
# Cible : la machine designee, ou le groupe si l'inventaire en contient un.
CIBLE_MODELE = $(if $(MODELE_HOTE),$(MODELE_HOTE),$(GROUPE_MODELE))
# Un inventaire d'UN SEUL HOTE (`-i "<ip>,"`) ne porte aucun `group_vars` : il faut donc
# lui donner l'utilisateur de connexion, sinon Ansible tente le compte local de
# l'operateur. Defaut `ansible`, qui est le `ciuser` du gabarit.
MODELE_UTILISATEUR ?= ansible
UTILISATEUR_MODELE = $(if $(MODELE_HOTE),-e ansible_user=$(MODELE_UTILISATEUR) -e cible_modele=$(MODELE_HOTE),)
preparer-modele: ansible-runtime _modele-requis _verifier-acces-modele _verifier-privileges-modele ## Prepare le gabarit dore (VM de reference clonee pour chaque hote)
ansible-playbook -i $(INVENTAIRE_MODELE) $(UTILISATEUR_MODELE) $(PLAYBOOK_PREPARER_MODELE)
verifier-modele: ansible-runtime _modele-requis ## Verifie le gabarit dore
ansible-playbook -i $(INVENTAIRE_MODELE) $(UTILISATEUR_MODELE) $(PLAYBOOK_VERIFIER_MODELE)
nettoyer-modele: ansible-runtime _modele-requis ## Nettoie le gabarit avant capture — exige CONFIRMER=true
@if [[ "$(CONFIRMER)" != "true" ]]; then \
printf '%s\n' 'Refus: relancer avec CONFIRMER=true pour le nettoyage final du modele.'; \
exit 2; \
fi
ansible-playbook -i $(INVENTAIRE_MODELE) $(UTILISATEUR_MODELE) $(PLAYBOOK_NETTOYER_MODELE) -e template_cleanup_confirm=true
.PHONY: _verifier-acces-hote _verifier-privileges-hote _attendre-hote faits verifier-hote
# Deux ATTENTES ACTIVES, sans lesquelles on ne peut pas enchainer creation et
# deploiement — donc sans lesquelles `make myDay` ne peut pas reconstruire seul.
#
# 1. SSH. `make creer-vm` rend la main des que Proxmox a DEMARRE la VM, pas quand elle
# repond. Verifier l'acces aussitot echoue presque toujours sur une machine neuve —
# et le symptome trompe : a travers la frontiere, le TCP s'etablit (SYN proxy) et
# l'echec se lit « Connection timed out during banner exchange ».
#
# 2. Le verrou dpkg. L'image Debian lance ses propres mises a jour au premier
# demarrage et tient `/var/lib/dpkg/lock-frontend` plusieurs minutes. Le premier
# `apt` d'Ansible echoue alors sur un verrou, pas sur une vraie erreur.
#
# `unattended-upgrades.service` est volontairement ABSENT de la condition : c'est un
# DEMON (Type=simple), toujours `active`. L'y inclure rendait l'attente impossible a
# satisfaire — elle echouait au bout du delai, systematiquement. Seules `apt-daily*`
# sont des one-shot, et c'est le VERROU qui dit si dpkg est reellement occupe.
#
# Les deux sont des COURSES de premier demarrage, pas des defauts de conception. On les
# attend au lieu de les subir. ATTENTE_HOTE (secondes) borne chacune : depasser le
# delai reste un echec, pour ne pas transformer une panne en attente infinie.
ATTENTE_HOTE ?= 600
_attendre-hote: ansible-runtime
@set -e; \
if [[ -z "$(LIMITE)" ]]; then printf '%s\n' 'Refus: LIMITE requis.'; exit 2; fi; \
fin=$$(( SECONDS + $(ATTENTE_HOTE) )); \
printf '%s' "Attente de SSH sur $(LIMITE) "; \
until ansible -i $(INVENTAIRE_PRODUCTION) $(LIMITE) -m ping -e ansible_become=false >/dev/null 2>&1; do \
if (( SECONDS > fin )); then printf '%s\n' " ECHEC: injoignable apres $(ATTENTE_HOTE)s."; exit 4; fi; \
printf '.'; sleep 5; \
done; \
printf '%s\n' " ok"; \
printf '%s' "Attente de cloud-init et des maj automatiques "; \
until ansible -i $(INVENTAIRE_PRODUCTION) $(LIMITE) -b -m shell -a \
'cloud-init status --wait >/dev/null 2>&1; \
! systemctl is-active --quiet apt-daily.service apt-daily-upgrade.service \
&& ! fuser /var/lib/dpkg/lock-frontend >/dev/null 2>&1' >/dev/null 2>&1; do \
if (( SECONDS > fin )); then printf '%s\n' " ECHEC: dpkg toujours occupe apres $(ATTENTE_HOTE)s."; exit 4; fi; \
printf '.'; sleep 10; \
done; \
printf '%s\n' " libere"
_verifier-acces-hote: ansible-runtime
ansible -i $(INVENTAIRE_PRODUCTION) $(LIMITE) -m ping -e ansible_become=false
_verifier-privileges-hote: ansible-runtime
ansible -i $(INVENTAIRE_PRODUCTION) $(LIMITE) -b -m command -a "whoami"
faits: ansible-runtime ## Interroge les faits Ansible de la flotte — LIMITE=<motif>
ansible -i $(INVENTAIRE_PRODUCTION) $(LIMITE) -m setup -a "filter=ansible_distribution*"
verifier-hote: ansible-runtime ## Passe le playbook de verification sur un hote
ansible-playbook -i $(INVENTAIRE_PRODUCTION) $(PLAYBOOK_VERIFIER_HOTE) $(OPTIONS_PLAYBOOK)