Set-OPS-Public/Makefile
Daniel Allaire e8907285d5 publier : les releases voyagent jusqu'aux deux forges ; wiki publie sur la forge du site
Le colis du genome ne portait que la branche : les etiquettes de release
n'arrivaient jamais sur la forge du site. Il les porte desormais, le runner les
pousse sans forcer, et la forge est relue par son API. publier.py pousse aussi
les etiquettes vers eregion. Le wiki, jamais publie sur la forge du site, l'est.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 19:04:31 -04:00

1924 lines
105 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.
#
# VIDE, ON N'EXPORTE RIEN (2026-10-04). Sur un clone sans aucune cle (la CI de la forge),
# `voutes.py` ne rend rien, et l'export posait quand meme `ANSIBLE_VAULT_IDENTITY_LIST=`.
# Ansible lit cette chaine vide comme un FICHIER de mot de passe, la resout en repertoire
# courant et refuse (« can not be a directory ») : `make ci` mourait a sa premiere commande
# sur un clone nu. Le poste a toujours des cles, il ne pouvait pas le voir.
SETOPS_VOUTES_IDENTITES := $(shell python3 scripts/voutes.py identites 2>/dev/null)
ifneq ($(strip $(SETOPS_VOUTES_IDENTITES)),)
export ANSIBLE_VAULT_IDENTITY_LIST ?= $(SETOPS_VOUTES_IDENTITES)
endif
# 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' 'Remise au client — EN DEUX TEMPS (docs/remise-au-client.md)'
@printf '%s\n' ' Temps 1, l IDENTITE : le client gouverne ses gens des le premier jour.'
@printf '%s\n' ' make remise-recenser # ce qui partirait, sans rien ecrire'
@printf '%s\n' ' make ca-racine ; make ca-empreinte # sa racine d AC, et le temoin a comparer'
@printf '%s\n' ' make remise-paquet VERS=/media/<toi>/REMISE'
@printf '%s\n' ' make remise-inscrire RECU_PAR="Prenom Nom" COURRIEL="…"'
@printf '%s\n' ' Temps 2, la MACHINE : sa cle entre, la notre sort, la voute est re-cletee.'
@printf '%s\n' ' make remise-verifier # second temps du, ou echu ?'
@printf '%s\n' ' make remise-recleer CONFIRMER=true # mesure la revocation, puis estampille'
@printf '%s\n' ''
@printf '%s\n' 'Wiki pedagogique'
@printf '%s\n' ' Publier wiki/ dans le wiki Forgejo (source versionnee -> vue browsable):'
@printf '%s\n' ' make wiki-publier WIKI_REMOTE=https://forge.<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_temoins.py
python3 scripts/tests/test_genome_colis.py
python3 scripts/tests/test_adressage_derive.py
python3 scripts/tests/test_gui_intrants.py
python3 scripts/tests/test_runbooks.py
python3 scripts/tests/test_portee_console.py
python3 scripts/tests/test_devis_placement.py
python3 scripts/tests/test_sonder.py
python3 scripts/tests/test_fraicheur_icinga.py
python3 scripts/tests/test_sondes_conditionnelles.py
python3 scripts/tests/test_sondes_syntaxe.py
python3 scripts/tests/test_empreinte_plancher.py
python3 scripts/tests/test_expositions_sans_port.py
python3 scripts/tests/test_proxmox_fw_externe.py
python3 scripts/tests/test_sonde_tcp.py
python3 scripts/tests/test_restauration.py
python3 scripts/tests/test_repertoires_partages.py
python3 scripts/tests/test_frontiere_refus.py
python3 scripts/tests/test_contexte.py
python3 scripts/tests/test_appels_locataire.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>
@# `-z "$(GROUPE)"` NE POUVAIT JAMAIS SE DECLENCHER (2026-10-03) : `GROUPE ?=
@# $(GROUPE_DEBIAN)` lui donne toujours une valeur. Une commande coupee par un retour
@# a la ligne (`make appliquer` seul) a ainsi applique `serveur_debian` a toute la flotte
@# de Technolibre. On refuse donc un groupe que l'operateur n'a pas DONNE lui-meme.
@if [[ "$(origin GROUPE)" == "file" || -z "$(GROUPE)" ]]; then \
printf '%s\n' 'Refus: relancer avec GROUPE=nom_groupe (aucun groupe par defaut ici).'; \
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)
@# `ARGS` comme chez `site-appliquer` (2026-10-03) : simuler (`ARGS="--check --diff"`)
@# avant d'appliquer doit etre possible par la meme porte que l'application elle-meme.
ansible-playbook -i $(INVENTAIRE_PRODUCTION) "$(DOSSIER_PLAYBOOKS_GROUPES)/$(GROUPE).yml" --limit '$(GROUPE):&$(GROUPE_HOTES_ACTIFS)' $(ARGS)
# 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 fiches-site-deposer
cles-recenser: ## Montre ce qui n'existe QUE sur ce poste (sans rien ecrire)
python3 scripts/exporter_cles.py --recenser
voutes-recenser: ## Montre les voutes de ce poste (gitignorees : sur aucune forge), sans rien ecrire
python3 scripts/exporter_voutes.py --recenser
voutes-exporter: ## Sort les voutes (deja chiffrees) vers un support, relues — VERS=<repertoire> NOM=<base>
@if [ -z "$(VERS)" ]; then printf '%s\n' 'Refus: VERS=<repertoire du support> requis.'; exit 2; fi
python3 scripts/exporter_voutes.py --vers "$(VERS)" $(if $(NOM),--nom "$(NOM)")
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 voutes-recenser voutes-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
remise-recenser: ## Ce qu'une remise au client emporterait, sans rien ecrire
python3 scripts/remise.py recenser
remise-paquet: ## Temps 1 : fabrique le paquet de remise, chiffre et relu — VERS=<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 — et c'est elle
@# qui voyage par un AUTRE canal que le support.
@if [ -z "$(VERS)" ]; then \
printf 'Refus: relancer avec VERS=<repertoire de destination>.\n'; \
printf 'Ex. make remise-paquet VERS=/media/danallaire/REMISE\n'; exit 2; fi
python3 scripts/remise.py paquet --vers "$(VERS)" $(if $(SUPPORT_CHIFFRE),--support-chiffre)
remise-inscrire: ## Inscrit la remise chez le locataire — RECU_PAR="…" COURRIEL="…" [DANS=30]
@if [ -z "$(RECU_PAR)" ] || [ -z "$(COURRIEL)" ]; then \
printf 'Refus: relancer avec RECU_PAR="Prenom Nom" COURRIEL="…".\n'; \
printf 'Le responsable designe (D-18) se NOMME au moment de la remise.\n'; exit 2; fi
python3 scripts/remise.py inscrire --recu-par "$(RECU_PAR)" --courriel "$(COURRIEL)" \
$(if $(DANS),--dans "$(DANS)",)
remise-verifier: ## Etat de la remise : temps 1 fait ? temps 2 du, ou echu ?
python3 scripts/remise.py verifier
remise-recleer: ## Temps 2 : mesure la revocation reelle, puis estampille — CONFIRMER=true
@# IL MESURE AVANT D'ESTAMPILLER. Un registre qui dirait « revoque » pendant que le
@# plan garde la cle de l'hebergeur flatterait tout le monde : c'est le seul mensonge
@# que ce fichier puisse porter sans que personne ne s'en apercoive.
python3 scripts/remise.py recleer $(if $(filter true,$(CONFIRMER)),--confirmer)
.PHONY: remise-recenser remise-paquet remise-inscrire remise-verifier remise-recleer
cacher-paquets: ## Tire les paquets NON DEBIAN dans le cache du controleur (a faire en ligne)
@# CE QUI REND LA RECONSTRUCTION HORS LIGNE POSSIBLE. Trois depots tiers sont en
@# HTTPS et contournent le cache du site (`Acquire::https::Proxy "DIRECT"`). Sans
@# cette cible, chaque VM neuve va chercher `step-cli` et `alloy` sur Internet — deux
@# integrations UNIVERSELLES. A lancer depuis un poste connecte ; le deploiement, lui,
@# n'en aura plus besoin.
python3 scripts/cacher_paquets.py
.PHONY: flux
flux: ansible-runtime ## Regenere le registre des flux et les regles nftables depuis les meta/flux.yml
python3 scripts/resoudre_flux.py registre
python3 scripts/resoudre_flux.py nftables
@# Les machines du SITE aussi : leur inventaire est dynamique, donc invisible
@# pour la generation qui lit un `hosts.yml`. Deux absences se cachaient l'une
@# l'autre — aucune regle generee, et aucun pare-feu applique pour s'en rendre
@# compte. Sans underlay monte, la cible ne fait rien plutot que d'echouer.
@python3 scripts/resoudre_flux.py nftables-site || printf 'note : pas de site monte, aucune regle de site generee.\n'
.PHONY: devis-reseau
devis-reseau: ansible-runtime ## Devis switch (VLANs/SVIs/ACLs) du reseau converge, derive des nomenclatures. DIALECTE=cisco|binardat
python3 scripts/devis_reseau.py $(if $(DIALECTE),--dialecte $(DIALECTE),)
.PHONY: devis-opnsense devis-opnsense-verifier
devis-opnsense: ansible-runtime ## Devis OPNsense (frontiere nord/sud), derive du registre des flux (pair: externe)
python3 scripts/devis_opnsense.py $(if $(JSON),--json,)
.PHONY: proxmox-fw-plan proxmox-fw-appliquer
proxmox-fw-plan: ansible-runtime ## Ecart entre le pare-feu est-ouest Proxmox et son devis (aucune ecriture) — ARGS="--objets-seulement" ou "--vm <vmid>"
python3 scripts/appliquer_proxmox_fw.py $(ARGS)
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 $(ARGS)
proxmox-fw-eprouver: ansible-runtime ## Verifie qu'activer le pare-feu d'UNE VM ne coupe rien (aucune ecriture) — TENANT=<OPS-X> HOTE=<vm>
@if [ -z "$(TENANT)" ] || [ -z "$(HOTE)" ]; then printf '%s\n' 'Refus: TENANT=<OPS-X> et HOTE=<vm> requis.'; exit 2; fi
python3 scripts/eprouver_parefeu.py --instance "$(TENANT)" --hote "$(HOTE)" --plan
proxmox-fw-activer-vm: ansible-runtime ## Active le pare-feu d'UNE VM en le prouvant avant/apres — TENANT= HOTE= CONFIRMER=true
@# UNE VM A LA FOIS, PREUVE COMPRISE (2026-09-28). Refuse si un flux reel echappe aux
@# regles ou si un flux echoue deja ; rejoue la matrice apres et lit Icinga.
@if [ -z "$(TENANT)" ] || [ -z "$(HOTE)" ]; then printf '%s\n' 'Refus: TENANT=<OPS-X> et HOTE=<vm> requis.'; exit 2; fi
@if [[ "$(CONFIRMER)" != "true" ]]; then printf '%s\n' "Refus: active le pare-feu de $(HOTE) en REJECT. Voir d'abord 'make proxmox-fw-eprouver', puis CONFIRMER=true."; exit 2; fi
python3 scripts/eprouver_parefeu.py --instance "$(TENANT)" --hote "$(HOTE)" --activer
.PHONY: proxmox-fw-eprouver proxmox-fw-activer-vm
.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) [TENANT=<depot>]
python3 scripts/devis_placement.py $(if $(TENANT),--locataire $(TENANT))
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
face-reseau-publier: ## Chaque locataire publie sa face reseau pour son site (a commiter chez lui, apres `make flux`)
@# LE LOCATAIRE PUBLIE, LE SITE LIT (2026-10-05). Le site lisait les fichiers internes de ses
@# locataires ; il lit desormais `face-reseau.yml`. P92 refuse une face perimee.
python3 scripts/contexte.py --publier-faces
fiches-site-deposer: ## Le site depose chez chacun de ses locataires la fiche qu'il lui destine (a commiter chez eux)
@# LE SITE DEPOSE, LE LOCATAIRE LIT (2026-10-05). Le runner d'un locataire n'a pas le depot
@# de son site : ce que le site lui attribue et lui offre lui parvient par ce fichier,
@# `fiche-site.yml`, que l'instancier lit. P91 refuse une fiche perimee.
python3 scripts/contexte.py --deposer-fiches
.PHONY: vpn-admin-plan vpn-admin-appliquer
vpn-admin-plan: ## Acces WireGuard des admins : ce que la frontiere porte face au plan (aucune ecriture)
python3 scripts/vpn_admin.py plan
vpn-admin-appliquer: ## Pose l'instance et les pairs declares, retire ceux qui ne le sont plus — CONFIRMER=true
CONFIRMER=$(CONFIRMER) python3 scripts/vpn_admin.py appliquer
.PHONY: dnssec-ds dnssec-verifier
dnssec-ds: ## DS a remettre au registraire, calcules depuis la voute du locataire (aucune ecriture)
python3 scripts/dnssec.py ds $(ZONE)
dnssec-verifier: ## Cle en voute pour chaque zone signee, et DS du registre conforme ? (aucune ecriture)
python3 scripts/dnssec.py verifier
.PHONY: dns-bascule-devis
dns-bascule-devis: ## Basculer nos serveurs de noms changerait-il quelque chose ? Plan contre DNS en service (aucune ecriture)
python3 scripts/dns_bascule.py $(if $(SERVEUR),--serveur $(SERVEUR)) $(if $(ZONE),--zone $(ZONE))
.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: restauration-etat restauration-renoncer sauvegarder-maintenant temoins-etat
sauvegarder-maintenant: ansible-runtime ## Depose l'etat de chaque noeud TOUT DE SUITE (avant de raser) — HOTE=<hote> optionnel
@set -e; $(VAULT_UNE_FOIS) \
ansible-playbook -i $(INVENTAIRE_PRODUCTION) playbooks/backup/restauration.yml \
-e restauration_action=sauvegarder $(if $(HOTE),--limit "$(HOTE)")
# LA RESTAURATION DANS LA RECONSTRUCTION (2026-09-30) — la remise elle-meme n'a pas de
# cible : chaque role proprietaire la fait au deploiement. Ces deux cibles-ci servent a
# VOIR ce qui attend, et a ECARTER un etat qu'on ne veut pas remettre.
restauration-etat: ansible-runtime ## Ce qui attend dans le depot, et ce que chaque jeu est devenu — HOTE=<hote> optionnel
@set -e; $(VAULT_UNE_FOIS) \
ansible-playbook -i $(INVENTAIRE_PRODUCTION) playbooks/backup/restauration.yml \
$(if $(HOTE),--limit "$(HOTE)")
# LES TEMOINS (2026-10-07) — l'etat remis est-il celui d'avant ? Compare l'etat vivant a
# l'instantane etiquete au rasage (`sauvegarder-maintenant`). `SOURCE=candidat` compare au
# dernier instantane d'avant la naissance du noeud, faute d'etiquette. Lecture seule.
temoins-etat: ansible-runtime ## L'etat vivant est-il celui d'avant le rasage ? (lecture seule) — HOTE= SOURCE=candidat optionnels
@set -e; $(VAULT_UNE_FOIS) \
ansible-playbook -i $(INVENTAIRE_PRODUCTION) playbooks/backup/restauration.yml \
-e restauration_action=temoins $(if $(SOURCE),-e restauration_source="$(SOURCE)") \
$(if $(HOTE),--limit "$(HOTE)")
restauration-renoncer: ansible-runtime ## Ecarte l'etat anterieur d'un jeu, sans le remettre — HOTE= JEU= CONFIRMER=true
@set -e; \
if [[ "$(CONFIRMER)" != "true" || -z "$(HOTE)" || -z "$(JEU)" ]]; then \
printf '%s\n' 'Refus: ECARTER un etat anterieur (il sortira de la retention). HOTE=, JEU= et CONFIRMER=true requis.'; \
exit 2; \
fi; \
$(VAULT_UNE_FOIS) \
ansible-playbook -i $(INVENTAIRE_PRODUCTION) playbooks/backup/restauration.yml \
--limit "$(HOTE)" -e restauration_action=renoncer -e restauration_jeu="$(JEU)"
.PHONY: schema
schema: ## Regenere docs/audit/schema-plan.json depuis les registres et les validateurs
python3 scripts/schema_plan.py
.PHONY: wiki-publier
wiki-publier: ## Publie le wiki (wiki/) vers la forge
@# CHAQUE RUNNER PUBLIE POUR SA FORGE (2026-09-14). Sans `WIKI_REMOTE`, l'adresse se
@# DERIVE de l'ecosysteme monte : le nom que son plan expose pour `serveur_forgejo`.
@#
@# DEUX NOTIONS A NE PAS CONFONDRE. La forge AMONT est celle ou l'on LIT le genome ; MA
@# forge est celle ou l'on SERT les siens. Le runner d'un locataire lit chez son
@# hebergeur et sert chez lui — un employe de TechnoLibre lit la forge de TechnoLibre.
@#
@# `WIKI_REMOTE` L'EMPORTE TOUJOURS, et c'est necessaire : le poste du mainteneur n'est
@# le runner d'aucun ecosysteme, et publie vers le domicile PUBLIC du projet, qui n'est
@# la forge d'aucun plan.
@#
@# `$(WIKI_REMOTE)`, PAS `$$remote` — CORRIGE LE 2026-09-14. La ligne lisait une variable
@# de SHELL homonyme, que rien ne definit. `WIKI_REMOTE` etait documente CINQ fois dans
@# ce Makefile, dont dans le message d'erreur qui le proposait comme remede — et la
@# cible ne l'a jamais lu. L'echappatoire documentee n'existait pas.
@#
@# `WIKI_BRANCHE`, deux cibles plus bas, etait ecrit correctement depuis le debut. C'est
@# en les mettant cote a cote que ca se voit : une option qui se lit autrement que sa
@# voisine est l'endroit ou regarder.
@set -e; \
remote="$(WIKI_REMOTE)"; \
if [[ -z "$$remote" ]]; then \
remote="$$(python3 scripts/ma_forge.py 2>/dev/null || true)"; \
fi; \
if [[ -z "$$remote" ]]; then \
printf '%s\n' 'Refus: aucune forge — ni WIKI_REMOTE, ni derivable du plan monte.'; \
python3 scripts/ma_forge.py >/dev/null || true; \
printf '%s\n' 'Ex: make wiki-publier WIKI_REMOTE=ssh://git@forge.<domaine>/<proprio>/<depot>.wiki.git'; \
exit 2; \
fi; \
printf 'Forge visee : %s\n' "$$remote"; \
src="$(CURDIR)/wiki"; \
tmp="$$(mktemp -d)"; \
trap 'rm -rf "$$tmp"' EXIT; \
printf '%s\n' "Clonage du wiki: $$remote"; \
if ! git clone --quiet --depth 1 "$$remote" "$$tmp/wiki"; then \
printf '%s\n' 'Echec du clone (URL ou acces ?). Le wiki doit exister (creer une 1re page dans Forgejo).'; \
exit 1; \
fi; \
: ' UN WIKI VIDE SE CLONE TRES BIEN, et le garde-fou ci-dessus ne le voyait pas :'; \
: ' il ne teste que l ECHEC du clone, alors que son message parle du cas VIDE.'; \
: ' Consequence mesuree le 2026-09-08 contre un depot bare local : la recette'; \
: ' allait au bout et creait une branche `master` — parce que `init.defaultBranch`'; \
: ' vaut master sur ce poste — alors que le wiki deja publie vit sur `main`. Le nom'; \
: ' de branche etait DEVINE, et il divergeait d une forge a l autre.'; \
: ' On refuse donc plutot que de choisir a la place de Forgejo : creer une premiere'; \
: ' page dans son interface initialise le wiki avec LA branche qu il attend.'; \
: ' ON NE DEVINE PAS — ON DEMANDE. Forgejo DECLARE la branche qu il attend, dans'; \
: ' `wiki_branch` de son API : GET /api/v1/repos/<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 $$remote"; \
exit 2; \
fi; \
fi; \
find "$$tmp/wiki" -maxdepth 1 -name '*.md' -delete; \
for f in "$$src"/*.md; do \
bn="$$(basename "$$f")"; \
[[ "$$bn" == "README.md" ]] && continue; \
cp "$$f" "$$tmp/wiki/$$bn"; \
done; \
rm -rf "$$tmp/wiki/img"; \
if compgen -G "$$src/img/*-annote.svg" > /dev/null; then \
mkdir -p "$$tmp/wiki/img"; \
cp -L "$$src"/img/*-annote.svg "$$tmp/wiki/img/"; \
fi; \
cd "$$tmp/wiki"; \
sha="$$(git -C "$(CURDIR)" rev-parse --short HEAD 2>/dev/null || echo inconnu)"; \
if [[ -z "$$(git status --porcelain)" ]]; then \
printf '%s\n' 'Wiki deja a jour (aucun changement).'; \
else \
git add -A; \
git commit --quiet -m "Publication du wiki depuis le depot (source: $$sha)"; \
git push --quiet; \
printf '%s\n' 'Wiki publie.'; \
fi; \
: ' LE TEMOIN. Sans lui, l ecart entre wiki/ et la forge n est mesurable par'; \
: ' personne : il s est creuse de 27 jours en silence avant que P60 n existe.'; \
: ' ECRIT DANS LES DEUX BRANCHES, et la seconde est la moins evidente. « Deja a'; \
: ' jour » est precisement le cas ou le temoin doit dire que la forge est au'; \
: ' niveau du depot : sortir la sans rien deposer laissait P60 rouge apres une'; \
: ' publication reussie, ce qui apprend a ignorer la preuve. Paye une fois.'; \
: ' APRES le push : un temoin ne doit jamais dire qu on a publie si on a echoue.'; \
{ printf '%s\n' '---' \
'# Ecrit par `make wiki-publier`, lu par la preuve P60. Ne pas editer a la main.' \
"remote: $$remote" \
"source: $$sha" \
"date: $$(date +%F)"; } > "$(CURDIR)/docs/audit/wiki-publie.yml"; \
printf '%s\n' 'Temoin depose : docs/audit/wiki-publie.yml (a committer).'
deployer-tout: _instance-requise ## Deploie TOUTE la flotte dans l'ordre des couches
@set -e; \
if [[ "$(CONFIRMER)" != "true" ]]; then \
printf '%s\n' 'Refus: deploiement ORCHESTRE de TOUTE la flotte (action impactante).'; \
printf '%s\n' 'Relancer avec CONFIRMER=true. Astuce: tester d abord en idempotent avec MODE_CHECK=1.'; \
exit 2; \
fi; \
python3 scripts/verifier_genome_a_jour.py || exit 2; \
python3 scripts/orchestrer.py verifier; \
python3 scripts/orchestrer.py ecrire; \
vault_chiffre="$$(grep -rlsIF '$$ANSIBLE_VAULT' $(dir $(INVENTAIRE_PRODUCTION))group_vars 2>/dev/null | head -1 || true)"; \
if [[ -n "$$vault_chiffre" && -z "$${ANSIBLE_VAULT_PASSWORD_FILE:-}$${ANSIBLE_VAULT_IDENTITY_LIST:-}" ]]; then \
if [[ -t 0 ]]; then \
read -r -s -p 'Mot de passe du vault Ansible: ' mdp; echo; \
vf="$$(mktemp)"; printf '%s' "$$mdp" > "$$vf"; chmod 600 "$$vf"; \
export ANSIBLE_VAULT_PASSWORD_FILE="$$vf"; \
trap 'rm -f "$$vf"' EXIT; \
else \
printf '%s\n' 'Refus: vault chiffre detecte mais aucun mot de passe (entree non interactive). Fournir ANSIBLE_VAULT_PASSWORD_FILE ou le champ vault de la GUI.'; \
exit 2; \
fi; \
fi; \
$(MAKE) _verifier-acces-hote LIMITE="$(GROUPE_HOTES_ACTIFS)"; \
$(MAKE) _verifier-privileges-hote LIMITE="$(GROUPE_HOTES_ACTIFS)"; \
mode="$$([[ -n "$(MODE_CHECK)" ]] && printf -- '--check --diff' || true)"; \
ansible-playbook -i $(INVENTAIRE_PRODUCTION) playbooks/site.yml --limit "$(GROUPE_HOTES_ACTIFS)" $$mode
# --- Reconstruction from-zero : creer TOUTES les VM (2a) puis deployer (2b) ---
.PHONY: flotte-creer
# CREATION EN PARALLELE, `PARALLELE=n` pour ajuster (defaut 4).
#
# Mesure du 2026-08-10 : 14 VM creees une par une, 1 min 35 s chacune, 22 min 08 s au
# total — soit 30 % d'une reconstruction complete. Et ce temps est surtout de l'ATTENTE :
# clone, demarrage, SSH, verrou dpkg. Quatorze fois de suite, sans recouvrement.
#
# LA SORTIE DE CHAQUE HOTE VA DANS SON PROPRE FICHIER, recopiee en bloc a la fin. Quatre
# clones ecrivant en meme temps sur la meme sortie donneraient un journal illisible — et
# apres une journee passee a traquer des diagnostics masques, ce serait un comble. On
# garde donc le marqueur `=== Creation VM: <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
# LE SITE MATERIALISE UN LOCATAIRE NOMME, D'APRES SA FACE (2026-10-05, docs/conception-contextes.md,
# materialisation M2). Avec `TENANT=<depot>`, la liste des machines et leurs parametres de clonage
# viennent de `face-reseau.yml` que le locataire publie — rendus a l'octet pres comme
# `parametres-proxmox` (P93) — et aucune instance n'est montee. Sans `TENANT`, l'ancienne lecture
# de l'instance montee (le poste, le laboratoire).
LISTE_ACTIFS = $(if $(TENANT),python3 scripts/contexte.py --hotes-actifs $(TENANT),python3 scripts/inventory_host.py --inventaire $(INVENTAIRE_PRODUCTION) lister-actifs)
PARAMS_CLONAGE = $(if $(TENANT),python3 scripts/contexte.py --parametres-clonage $(TENANT) --hote,python3 scripts/inventory_host.py --inventaire $(INVENTAIRE_PRODUCTION) parametres-proxmox --hote)
INSTANCE_OU_LOCATAIRE = $(if $(TENANT),,_instance-requise)
flotte-creer: $(INSTANCE_OU_LOCATAIRE) ## 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="$$($(LISTE_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" $(if $(TENANT),TENANT="$(TENANT)") > "$$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'
locataire-creer: ## Le SITE cree les VM d'un locataire d'apres sa face reseau — TENANT=<depot> CONFIRMER=true [PARALLELE=n]
@if [[ -z "$(TENANT)" ]]; then printf '%s\n' 'Refus: TENANT=<depot du locataire> requis.'; exit 2; fi
@$(MAKE) --no-print-directory flotte-creer TENANT="$(TENANT)" CONFIRMER="$(CONFIRMER)" PARALLELE="$(PARALLELE)"
locataire-raser: ## DESTRUCTIF : le SITE rase les VM d'un locataire d'apres sa face reseau — TENANT=<depot> CONFIRMER=true INSTANCE=<nom court> [HOTE=h]
@if [[ -z "$(TENANT)" ]]; then printf '%s\n' 'Refus: TENANT=<depot du locataire> requis.'; exit 2; fi
@$(MAKE) --no-print-directory raser TENANT="$(TENANT)" CONFIRMER="$(CONFIRMER)" INSTANCE="$(INSTANCE)" HOTE="$(HOTE)"
# --- 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|...>
@if [[ "$(origin GROUPE)" == "file" || -z "$(GROUPE)" ]]; then \
printf '%s\n' 'Refus: relancer avec GROUPE=nom_groupe (aucun groupe par defaut ici).'; \
exit 2; \
fi
@# `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
publier: ## Publie le genome d'un geste : eregion PUIS la forge du site, et le verifie — DEPOT=<nom> pour un seul
@# LE RETARD VIENT D'UN GESTE QU'ON OUBLIE (2026-09-28) : un `git push` vers eregion ne
@# met pas la forge du site a jour, et c'est elle qui fait autorite. Refuse si eregion
@# porte des commits absents du poste (une fusion acceptee) — ne pas les ecraser.
python3 scripts/publier.py $(if $(DEPOT),--depot "$(DEPOT)")
.PHONY: publier
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: depots-perimes
depots-perimes: ansible-runtime _instance-requise ## Depots qu'un runner ne declare plus : regarde, et retire avec CONFIRMER=true
@# DESTRUCTIF SUR CONFIRMATION SEULEMENT. Sans `CONFIRMER=true`, ce geste REGARDE :
@# il nomme ce que le runner porte et que son plan ne declare plus, et dit ce qu'un
@# retrait emporterait. Trois choses ne sont jamais retirees, meme confirmees : ce
@# qui n'est pas un depot git, ce qui porte des modifications non validees, et ce
@# qui porte des commits qu'aucun distant ne porte.
CONFIRMER="$(CONFIRMER)" ansible-playbook -i $(INVENTAIRE_PRODUCTION) \
playbooks/maintenance/depots_perimes.yml $(if $(HOTE),--limit $(HOTE),) $(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_OU_LOCATAIRE) ## DESTRUCTIF : detruit les VM derivees du plan — exige CONFIRMER=true ET INSTANCE=<nom> [TENANT=<depot>]
@python3 scripts/raser.py $(if $(TENANT),--locataire $(TENANT)) $(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) monter-flotte CONFIRMER=true
# MONTER UNE FLOTTE DONT LES VM EXISTENT DEJA (2026-09-30). C'est la seconde moitie de
# `reconstruire`, et TOUT ce que le runner d'un locataire peut en faire : il ne cree pas de VM
# (l'API du cluster est a l'hebergeur), il les recoit, armees, et les monte. Jusqu'ici cette
# sequence vivait dans un script ecrit a la main dans /tmp du runner — deux reconstructions
# l'ont portee, et aucune trace ne la gardait.
#
# `set -e` : la premiere etape en echec arrete tout. Chaque etape est horodatee, pour qu'un
# journal dise ou l'on en etait sans relire dix mille lignes d'Ansible. `valider` fait partie
# de la sequence : une flotte sans recette n'est pas montee, elle est deployee.
.PHONY: monter-flotte
monter-flotte: _instance-requise ## Monte une flotte deja creee : flux, AC et DNS, tout le reste, recette — CONFIRMER=true
@set -e; \
if [[ "$(CONFIRMER)" != "true" ]]; then \
printf '%s\n' 'Refus: MONTER la flotte (deploiement complet puis recette). Relancer avec CONFIRMER=true.'; \
exit 2; \
fi; \
etape() { printf '\n=== ETAPE %s %s\n' "$$1" "$$(date +%T)"; }; \
etape flux; $(MAKE) flux; \
etape _amorcer-socle; $(MAKE) _amorcer-socle; \
etape deployer-tout; $(MAKE) deployer-tout CONFIRMER=true; \
etape valider; $(MAKE) valider; \
printf '\n=== FLOTTE MONTEE %s\n' "$$(date +%T)"
# RECONSTRUIRE UN LOCATAIRE DE BOUT EN BOUT, DEPUIS LE POSTE (2026-09-30). Les deux
# reconstructions du 2026-09-30 passaient par cinq endroits et des commandes tapees a la
# main ; la sequence vit desormais dans `scripts/reconstruire_locataire.py` (voir son
# en-tete). `TENANT=` designe l'ecosysteme en toutes lettres : le nom qu'exige `raser` en est
# derive, pas redemande. L'armement marque une pause, sauf `ARMER=oui`. `DEPUIS=<etape>` reprend apres un
# arret. Le journal complet va dans `<locataire>/logs/`.
.PHONY: reconstruire-locataire
reconstruire-locataire: ## Rase et reconstruit un locataire, du site a la recette, etat remis — TENANT=OPS-X CONFIRMER=true
@if [[ -z "$(TENANT)" ]]; then \
printf '%s\n' 'Refus: TENANT=<OPS-X> requis (le dossier du locataire, en toutes lettres).'; \
exit 2; \
fi
python3 scripts/reconstruire_locataire.py --tenant "$(TENANT)" \
$(if $(filter true,$(CONFIRMER)),--confirmer) $(if $(DEPUIS),--depuis "$(DEPUIS)") \
$(if $(filter oui,$(ARMER)),--armer)
# LE PARE-FEU PROXMOX D'UNE FLOTTE RECONSTRUITE (2026-09-30). Une VM clonee nait sans les
# options de pare-feu : chaque reconstruction les perd. La procedure (une VM a la fois,
# matrice avant/apres, Icinga) etait rejouee par une boucle tapee a la main ; `--flotte` la
# porte, avec la regle qui la rend sure : arret au premier refus.
#
# PAR LES SONDES DEPUIS LE 2026-09-30 (`--sondes`) : la matrice prenait une vingtaine de
# minutes en fin de reconstruction. Chaque VM teste desormais chaque minute ses flux promis
# (sonde `connectivite`) ; on active tout d'un coup et on lit Icinga. La matrice reste pour
# le diagnostic : `python3 scripts/eprouver_parefeu.py --instance OPS-X --hote <vm> --plan`.
#
# DEPUIS LE POSTE : la matrice se joue avec les acces du LOCATAIRE, l'activation passe par
# le runner du SITE, seul a joindre l'API du cluster. Le poste est le seul a tenir les deux.
.PHONY: parefeu-verifier-flotte parefeu-activer-flotte
parefeu-verifier-flotte: ## La sonde `connectivite` est-elle saine sur chaque VM (prealable a l'activation) — INSTANCE=OPS-X
@if [[ -z "$(INSTANCE)" ]]; then printf '%s\n' 'Refus: INSTANCE=<OPS-X> requis.'; exit 2; fi
python3 scripts/eprouver_parefeu.py --instance "$(INSTANCE)" --flotte --sondes --plan
parefeu-activer-flotte: ## Active le pare-feu Proxmox de toutes les VM, juge par la sonde `connectivite` — INSTANCE=OPS-X CONFIRMER=true
@if [[ -z "$(INSTANCE)" || "$(CONFIRMER)" != "true" ]]; then \
printf '%s\n' 'Refus: INSTANCE=<OPS-X> et CONFIRMER=true requis (active le pare-feu de TOUTES les VM, une a une).'; \
exit 2; \
fi
python3 scripts/eprouver_parefeu.py --instance "$(INSTANCE)" --flotte --sondes --activer
# « 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>
@# `-z "$(GROUPE)"` NE POUVAIT JAMAIS SE DECLENCHER (2026-10-03) : `GROUPE ?=
@# $(GROUPE_DEBIAN)` lui donne toujours une valeur. Une commande coupee par un retour
@# a la ligne (`make appliquer` seul) a ainsi applique `serveur_debian` a toute la flotte
@# de Technolibre. On refuse donc un groupe que l'operateur n'a pas DONNE lui-meme.
@if [[ "$(origin GROUPE)" == "file" || -z "$(GROUPE)" ]]; then \
printf '%s\n' 'Refus: relancer avec GROUPE=nom_groupe (aucun groupe par defaut ici).'; \
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 $(if $(TENANT),--pool-du-locataire $(TENANT),--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_OU_LOCATAIRE) ## Cree une VM et attend qu'elle soit joignable — HOTE=<nom> [TENANT=<depot>]
@set -e; \
if [[ -z "$(HOTE)" ]]; then \
printf '%s\n' 'Refus: relancer avec HOTE=nom_hote (declare dans le plan).'; \
exit 2; \
fi; \
params="$$($(PARAMS_CLONAGE) $(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="$$($(PARAMS_CLONAGE) $(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)