Set-OPS-Public/Makefile
Daniel Allaire 377462b141 voutes : une voute, une cle — separer avant de distribuer
Decision de l'exploitant : chaque runner est maitre de sa voute et en detient la
cle. C'est ce qui rend un runner autonome, donc ce qui rend l'emancipation
atteignable. Elle exigeait un prealable, mesure ce matin : UN SEUL mot de passe
ouvrait les SIX voutes de la flotte, celle de la fabric comprise.

DISTRIBUER AVANT DE SEPARER AURAIT ETE PIRE QUE LE STATU QUO : poser « la » cle
sur chaque runner rendait chaque runner capable d'ouvrir les autres. Compromettre
le plus petit locataire donnait les secrets de l'hebergeur.

Cinq cles, une par ecosysteme, sous ~/.config/setops-vault-<depot>. Verification
croisee apres rechiffrement : la diagonale, et rien qu'elle. L'ancienne cle
maitresse n'ouvre plus aucune des six.

UN SEUL MOT DE PASSE NE POUVAIT PLUS SUFFIRE, et pas pour la raison qu'on croit :
`cloner_vm_debian.yml` charge la voute du TENANT puis celle de l'UNDERLAY dans la
meme execution. `ANSIBLE_VAULT_IDENTITY_LIST` en porte plusieurs et les essaie
toutes — un seul export suffit pour les 28 appels a ansible-playbook, sans en
toucher un seul. La liste se derive dans scripts/voutes.py.

CE QUI BORNE LE POUVOIR N'EST PAS LA LISTE MAIS LA PRESENCE DES FICHIERS. Sur le
poste, toutes les cles sont la — c'est l'humain qui les detient toutes, et P03 lit
les inventaires de tous les freres. Sur un runner, une seule existe. Le code est
identique, le pouvoir ne l'est pas.

DEUX PREUVES ONT DIT CE QUI MANQUAIT. P03 est tombee des la separation : elle lit
les inventaires voisins, donc il lui faut leurs cles — c'est elle qui a etabli que
la liste devait couvrir le voisinage. P16 s'est mise a SAUTER : sa garde ne
connaissait que ANSIBLE_VAULT_PASSWORD_FILE. Une preuve sautee se lit trop
facilement comme une preuve passee.

COUT ASSUME : le chiffre et sa cle cohabiteront sur la meme machine des que les
runners recevront la leur. Le pari tient parce que le perimetre est borne.

Les six voutes sauvegardees avant rechiffrement. L'ancienne cle maitresse reste
sur le poste : la retirer est une decision, pas un nettoyage.

make verifier : vert. make prouver : CONFORME, 52 OK, 0 echec, 0 saute — sans
ANSIBLE_VAULT_PASSWORD_FILE.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-08-28 14:23:33 -04:00

1269 lines
64 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
GROUPE_MODELE ?= modeles_vm
GROUPE_DEBIAN ?= serveur_debian
GROUPE_HOTES_ACTIFS ?= hotes_actifs
LIMITE ?= $(GROUPE_DEBIAN)
HOTE ?=
ADRESSE_IP ?=
GROUPES ?= $(GROUPE_DEBIAN)
GROUPE ?= $(GROUPE_DEBIAN)
UTILISATEUR_ANSIBLE ?= ansible
VMID_MODELE ?=
VMID ?=
NOEUD_PROXMOX ?=
STOCKAGE_PROXMOX ?=
FORMAT_DISQUE ?=
TAILLE_DISQUE ?=
DISQUE_PROXMOX ?=
CIDR ?= 24
PASSERELLE ?=
DNS ?=
DHCP ?= false
CIUSER ?=
CLE_SSH_PUBLIQUE ?=
# 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_INSTANCE)/inventories/production/hosts.yml'
@printf '%s\n' ' Verifier les inventaires:'
@printf '%s\n' ' make inventaire-verifier'
@printf '%s\n' ' Lister les donnees brutes:'
@printf '%s\n' ' make inventaire-lister'
@printf '%s\n' ' Interface locale de gestion:'
@printf '%s\n' ' make inventaire-ui'
@printf '%s\n' ''
@printf '%s\n' 'Modele Debian 13 Proxmox'
@printf '%s\n' ' Construire et verifier:'
@printf '%s\n' ' make preparer-modele'
@printf '%s\n' ' make verifier-modele'
@printf '%s\n' ' Nettoyage final protege:'
@printf '%s\n' ' make nettoyer-modele CONFIRMER=true'
@printf '%s\n' ''
@printf '%s\n' 'Validation'
@printf '%s\n' ' make syntaxe'
@printf '%s\n' ' make lint'
@printf '%s\n' ' make verifier'
@printf '%s\n' ''
@printf '%s\n' 'Variables frequentes'
@printf '%s\n' ' HOTE=web-frontal-01 GROUPE=serveur_debian GROUPES="serveur_debian serveur_durci"'
@printf '%s\n' ' VMID=95301 VLAN=15 ADRESSE_IP=10.0.2.31 PASSERELLE=10.0.2.1'
@printf '%s\n' ' FICHIER_INVENTAIRE=$(SETOPS_INSTANCE)/inventories/production/hosts.yml FICHIER_DEPENDANCES=docs/dependances-groupes.yml CONFIRMER=true'
.PHONY: lint
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 des 30 playbooks de groupe
@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
.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
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
.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: 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: 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; \
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"; \
if [[ -z "$$(git status --porcelain)" ]]; then \
printf '%s\n' 'Wiki deja a jour (aucun changement).'; \
exit 0; \
fi; \
git add -A; \
sha="$$(git -C "$(CURDIR)" rev-parse --short HEAD 2>/dev/null || echo inconnu)"; \
git commit --quiet -m "Publication du wiki depuis le depot (source: $$sha)"; \
git push --quiet; \
printf '%s\n' 'Wiki publie.'
deployer-tout: _instance-requise ## Deploie TOUTE la flotte dans l'ordre des couches
@set -e; \
if [[ "$(CONFIRMER)" != "true" ]]; then \
printf '%s\n' 'Refus: deploiement ORCHESTRE de TOUTE la flotte (action impactante).'; \
printf '%s\n' 'Relancer avec CONFIRMER=true. Astuce: tester d abord en idempotent avec MODE_CHECK=1.'; \
exit 2; \
fi; \
python3 scripts/orchestrer.py verifier; \
python3 scripts/orchestrer.py ecrire; \
vault_chiffre="$$(grep -rlsIF '$$ANSIBLE_VAULT' $(dir $(INVENTAIRE_PRODUCTION))group_vars 2>/dev/null | head -1 || true)"; \
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)
# --- LE GENOME REMONTE SUR LA FORGE DU SITE -----------------------------------
#
# La forge du genome vit DANS le site, sur un reseau que le poste de l'exploitant ne route
# pas — et c'est voulu : le poste n'a de patte que sur l'administration, la frontiere
# police le reste. Il ne peut donc pas pousser lui-meme.
#
# On ne perce pas un chemin pour lui. Le RUNNER est dans le site, il porte le genome, il a
# une identite : on lui PORTE les commits dans un `git bundle` — un fichier, verifiable,
# qui ne demande aucun reseau — et c'est LUI qui pousse, avec SA cle.
#
# Mesure du 2026-08-26 : la forge du site etait restee quatre commits derriere le poste,
# sans que rien ne le signale. Le runner clonait donc un moteur perime — dont, ce jour-la,
# le correctif qui desarme le pare-feu Proxmox. Un ecosysteme qui se reproduit depuis une
# forge en retard reproduit ses defauts.
#
# CE QUI EST POUSSE SE DERIVE de `serveur_ops_depots` (au plan du site) : on pousse ceux
# dont une copie locale existe a cote du moteur, et rien d'autre.
.PHONY: genome-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
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 \
HOTE="$$m" \
VMID="$$SETOPS_VMID" \
ADRESSE_IP="$$SETOPS_IP" \
CIDR="$$SETOPS_CIDR" \
PASSERELLE="$$SETOPS_PASSERELLE" \
VLAN="$${SETOPS_VLAN:-}" \
PONT_PROXMOX="$$SETOPS_PONT" \
STOCKAGE_PROXMOX="$${SETOPS_STOCKAGE:-$(STOCKAGE_PROXMOX)}" \
TAILLE_DISQUE="$$SETOPS_DISQUE" \
COEURS="$$SETOPS_COEURS" \
MEMOIRE="$$SETOPS_MEMOIRE" \
NOEUD_PROXMOX="$$SETOPS_NOEUD" \
VMID_MODELE="$$SETOPS_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="$$(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 "$(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)}" \
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: ## Amorce l'acces SSH aux serveurs neufs
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)