filtrage soumet GTUBE ; edge interroge chaque exposition en local. Sans serveur par defaut, un nom inconnu recevait Keycloak : 000-defaut.conf (444, rejet TLS). Garde test_sondes_syntaxe : chaque gabarit de sonde se rend en bash valide. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1768 lines
95 KiB
Makefile
1768 lines
95 KiB
Makefile
SHELL := /usr/bin/env bash
|
|
|
|
export ANSIBLE_HOME ?= $(CURDIR)/.ansible
|
|
export ANSIBLE_LOCAL_TEMP ?= $(CURDIR)/.ansible/tmp
|
|
export ANSIBLE_SSH_CONTROL_PATH_DIR ?= $(CURDIR)/.ansible/cp
|
|
export ANSIBLE_SSH_ARGS ?= -F /dev/null -o ControlMaster=no
|
|
|
|
export SETOPS_INSTANCE ?= instance
|
|
|
|
# UNE VOUTE, UNE CLE — ET ANSIBLE LES TIENT TOUTES A LA FOIS (2026-08-28).
|
|
#
|
|
# Un seul mot de passe ouvrait les six voutes de la flotte, celle de la fabric comprise.
|
|
# Depuis la separation, chaque ecosysteme a la sienne, sous
|
|
# `~/.config/setops-vault-<depot-en-minuscules>`.
|
|
#
|
|
# UN `ANSIBLE_VAULT_PASSWORD_FILE` NE PEUT PLUS SUFFIRE : `cloner_vm_debian.yml` charge
|
|
# la voute du TENANT puis celle de l'UNDERLAY dans la meme execution — les identifiants
|
|
# Proxmox appartiennent a l'hebergeur, le reste au locataire. `ANSIBLE_VAULT_IDENTITY_LIST`
|
|
# porte plusieurs secrets et les essaie tous ; un seul export suffit donc pour les 28
|
|
# appels a `ansible-playbook`, sans en toucher un seul.
|
|
#
|
|
# `?=` : une valeur deja posee dans l'environnement gagne — la GUI et les runners peuvent
|
|
# nommer leurs propres cles sans que ce fichier ait a les connaitre.
|
|
export ANSIBLE_VAULT_IDENTITY_LIST ?= $(shell python3 scripts/voutes.py identites 2>/dev/null)
|
|
# Inventaire de l'instance : un seul par instance dans le modèle « séparation par
|
|
# instance ». Détection rétro-compatible : principal > production > lab.
|
|
# Surchargeable : make … SETOPS_INVENTAIRE=chemin/hosts.yml
|
|
#
|
|
# LA PRÉCÉDENCE SUIT CELLE DU CODE (`instancier._inventaire`) : fichier existant, puis
|
|
# RÉPERTOIRE existant, puis défaut. Le troisième niveau manquait, et comme cette variable
|
|
# FORCE la cible, `make instancier` sur une instance neuve visait un `principal/`
|
|
# inexistant alors que le plan vivait dans `production/` — l'échec du tout premier geste
|
|
# du QUICKSTART, sur le modèle public (mesuré le 2026-08-19). Sans `make`, la même
|
|
# commande réussissait : c'est la variable exportée qui décidait, pas le code.
|
|
SETOPS_INVENTAIRE ?= $(firstword \
|
|
$(wildcard $(SETOPS_INSTANCE)/inventories/principal/hosts.yml) \
|
|
$(wildcard $(SETOPS_INSTANCE)/inventories/production/hosts.yml) \
|
|
$(patsubst %/,%/hosts.yml,$(wildcard $(SETOPS_INSTANCE)/inventories/principal/)) \
|
|
$(patsubst %/,%/hosts.yml,$(wildcard $(SETOPS_INSTANCE)/inventories/production/)) \
|
|
$(SETOPS_INSTANCE)/inventories/principal/hosts.yml)
|
|
export SETOPS_INVENTAIRE
|
|
# Inventaire « modèle » (construction du golden template) : lab > principal > production.
|
|
INVENTAIRE_LAB ?= $(firstword $(wildcard $(SETOPS_INSTANCE)/inventories/lab/hosts.yml) $(wildcard $(SETOPS_INSTANCE)/inventories/principal/hosts.yml) $(SETOPS_INSTANCE)/inventories/production/hosts.yml)
|
|
INVENTAIRE_PRODUCTION ?= $(SETOPS_INVENTAIRE)
|
|
FICHIER_INVENTAIRE ?= $(SETOPS_INVENTAIRE)
|
|
FICHIER_DEPENDANCES ?= docs/dependances-groupes.yml
|
|
|
|
# LES COUCHES DE L'INSEMINATION — CE QUE LE SITE POSE CHEZ UN TENANT, ET RIEN DE PLUS.
|
|
#
|
|
# Un ecosysteme neuf ne s'amorce pas lui-meme : quelqu'un doit poser sa premiere machine
|
|
# et lui donner de quoi continuer. Le runner du SITE le fait — socle, moteur, plan,
|
|
# plancher de resolution — puis s'arrete.
|
|
#
|
|
# CES DEUX-LA ET AUCUNE AUTRE, parce qu'elles ne demandent AUCUN SECRET. Le site ne
|
|
# detient pas la voute d'un tenant, et ne doit jamais la detenir : ce qui vient apres
|
|
# (`client_pki`, `serveur_ops_tenant`...) reclame des valeurs qu'il n'a pas. La liste
|
|
# n'est donc pas une commodite, c'est la LIGNE DE PARTAGE DU POUVOIR, et la preuve P54
|
|
# refuse toute couche qui y ferait entrer un secret.
|
|
COUCHES_INSEMINATION ?= serveur_debian serveur_ops
|
|
GROUPE_MODELE ?= modeles_vm
|
|
GROUPE_DEBIAN ?= serveur_debian
|
|
GROUPE_HOTES_ACTIFS ?= hotes_actifs
|
|
LIMITE ?= $(GROUPE_DEBIAN)
|
|
HOTE ?=
|
|
ADRESSE_IP ?=
|
|
GROUPES ?= $(GROUPE_DEBIAN)
|
|
GROUPE ?= $(GROUPE_DEBIAN)
|
|
UTILISATEUR_ANSIBLE ?= ansible
|
|
# LE GABARIT VIENT DU SITE, PAS DU TENANT (2026-09-01).
|
|
#
|
|
# « Les ressources du SITE font autorite pour tous ses artefacts. » Le gabarit en est un :
|
|
# c'est de lui que descend chaque VM de chaque tenant. Son VMID vivait pourtant dans les
|
|
# group_vars de l'ecosysteme — deux tenants pouvaient donc cloner deux gabarits differents
|
|
# sans que rien ne le dise, et un tenant decidait d'un objet qui n'est pas a lui.
|
|
#
|
|
# Meme mouvement que l'INDEX, tranche le 2026-08-25 : le site ALLOUE, le tenant RECOIT.
|
|
#
|
|
# `?=` : une valeur donnee en ligne de commande gagne — necessaire pour eprouver un
|
|
# gabarit neuf avant de le declarer. Et si le site n'en declare aucun, la valeur reste
|
|
# vide et le plan du tenant reprend la main : degrader, jamais deviner.
|
|
VMID_MODELE ?= $(shell python3 scripts/underlay.py --gabarit 2>/dev/null | sed -n 's/^vmid=//p')
|
|
# NOTE SUR LE PIEGE QU'ON VIENT DE FERMER : `creer-vm` passait
|
|
# `VMID_MODELE="$$SETOPS_VMID_MODELE"` a `cloner-vm`, or `inventory_host.py` n'emet PAS
|
|
# cette variable — elle valait donc le VIDE, et ce vide ECRASAIT la valeur derivee. Le
|
|
# gabarit du tenant reprenait la main sans que rien ne le dise. `:-` rend la derivation au
|
|
# site quand l'inventaire ne dit rien.
|
|
VMID ?=
|
|
NOEUD_PROXMOX ?=
|
|
# LE STOCKAGE DU CLONE SUIT CELUI DU GABARIT, ET IL EST DECLARE (2026-09-10).
|
|
#
|
|
# Vide, `qm clone` heritait SILENCIEUSEMENT du stockage de la source. Ca marchait, et
|
|
# c'est bien la le probleme : quand le gabarit a change de stockage, la destination a
|
|
# suivi sans qu'aucun fichier ne le dise. Le site le declare desormais
|
|
# (`gabarit.stockage`), et cette ligne le rend a la commande de clonage.
|
|
#
|
|
# `SETOPS_STOCKAGE` — le `stockage:` d'une machine au plan du tenant — garde la priorite
|
|
# (voir `creer-vm`) : un ecosysteme peut poser une machine ailleurs, ce qui n'est pas la
|
|
# meme question que « d'ou nait la flotte ».
|
|
STOCKAGE_PROXMOX ?= $(shell python3 scripts/underlay.py --gabarit 2>/dev/null | sed -n 's/^stockage=//p')
|
|
FORMAT_DISQUE ?=
|
|
TAILLE_DISQUE ?=
|
|
DISQUE_PROXMOX ?=
|
|
CIDR ?= 24
|
|
PASSERELLE ?=
|
|
DNS ?=
|
|
DHCP ?= false
|
|
CIUSER ?=
|
|
CLE_SSH_PUBLIQUE ?=
|
|
# LA CLE D'AMORCAGE VOYAGE PAR L'ENVIRONNEMENT, ET ARRIVE EN JSON. MESURE (2026-08-28).
|
|
#
|
|
# Une cle publique contient des ESPACES — type, materiel, commentaire — et deux couches
|
|
# les mangent en silence :
|
|
#
|
|
# `ansible-playbook -e cle=valeur` decoupe la chaine AU SHLEX : tout ce qui suit le
|
|
# premier espace devient d'autres paires cle=valeur. Le module recevait
|
|
# `"sshkeys": "ssh-ed25519"` — le premier mot, rien d'autre.
|
|
#
|
|
# Et une variable `make` passee a un sous-make est un chemin tout aussi fragile pour
|
|
# une valeur a espaces.
|
|
#
|
|
# Proxmox repondait `500 SSH public key validation error` : un message qui ne parle ni
|
|
# de shlex, ni de make, ni d'espaces. Le contrôle qui l'a isole : rejouer `cloner-vm`
|
|
# SANS la cle — la tache passe.
|
|
#
|
|
# D'ou la forme retenue : l'environnement (`SETOPS_CLES_AMORCAGE`, exporte par
|
|
# `creer-vm`) pour le transport, et `-e '{"...": "..."}'` en JSON pour l'entree dans
|
|
# Ansible — le seul mode de `-e` qui ne decoupe rien.
|
|
PONT_PROXMOX ?=
|
|
VLAN ?=
|
|
DEMARRER ?=
|
|
CLONE_COMPLET ?=
|
|
|
|
CONFIRMER ?= false
|
|
VERIFICATION ?= false
|
|
DIFF ?= false
|
|
ETIQUETTES ?=
|
|
SAUTER_ETIQUETTES ?=
|
|
VARIABLES ?=
|
|
|
|
OPTIONS_PLAYBOOK :=
|
|
ifneq ($(LIMITE),)
|
|
OPTIONS_PLAYBOOK += --limit $(LIMITE)
|
|
endif
|
|
ifeq ($(VERIFICATION),true)
|
|
OPTIONS_PLAYBOOK += --check
|
|
endif
|
|
ifeq ($(DIFF),true)
|
|
OPTIONS_PLAYBOOK += --diff
|
|
endif
|
|
ifneq ($(ETIQUETTES),)
|
|
OPTIONS_PLAYBOOK += --tags $(ETIQUETTES)
|
|
endif
|
|
ifneq ($(SAUTER_ETIQUETTES),)
|
|
OPTIONS_PLAYBOOK += --skip-tags $(SAUTER_ETIQUETTES)
|
|
endif
|
|
ifneq ($(VARIABLES),)
|
|
OPTIONS_PLAYBOOK += -e $(VARIABLES)
|
|
endif
|
|
|
|
PLAYBOOK_PREPARER_MODELE := playbooks/modeles_vm/debian13_proxmox_preparer.yml
|
|
PLAYBOOK_VERIFIER_MODELE := playbooks/modeles_vm/debian13_proxmox_verifier.yml
|
|
PLAYBOOK_NETTOYER_MODELE := playbooks/modeles_vm/debian13_proxmox_nettoyer.yml
|
|
PLAYBOOK_VERIFIER_HOTE := playbooks/maintenance/verifier_hote_debian.yml
|
|
PLAYBOOK_PROXMOX_CLONER_VM := playbooks/proxmox/cloner_vm_debian.yml
|
|
DOSSIER_PLAYBOOKS_GROUPES := playbooks/groupes
|
|
|
|
.DEFAULT_GOAL := aide
|
|
|
|
.PHONY: ansible-runtime
|
|
ansible-runtime: ## Prepare le repertoire temporaire local d'Ansible (prerequis interne des cibles qui deploient)
|
|
@mkdir -p "$(ANSIBLE_LOCAL_TEMP)"
|
|
@mkdir -p "$(ANSIBLE_SSH_CONTROL_PATH_DIR)"
|
|
|
|
.PHONY: _instance-requise
|
|
_instance-requise:
|
|
@if [[ ! -f "$(SETOPS_INSTANCE)/plan/serveurs.yml" ]]; then \
|
|
printf '%s\n' "Aucune instance configuree : '$(SETOPS_INSTANCE)/plan' introuvable."; \
|
|
printf '%s\n' "Demarre avec QUICKSTART.md. En bref :"; \
|
|
printf '%s\n' " cp -r exemples/modeles/<modele> ../mon-instance && ln -s ../mon-instance instance"; \
|
|
printf '%s\n' " modeles disponibles : $$(ls exemples/modeles 2>/dev/null | grep -v '\.md' | tr '\n' ' ')"; \
|
|
exit 2; \
|
|
fi
|
|
|
|
.PHONY: aide
|
|
aide: ## Affiche l'aide detaillee du moteur (au-dela de cette liste)
|
|
@printf '%s\n' 'Set-OPS — moteur d ecosystemes numeriques souverains'
|
|
@printf '%s\n' ''
|
|
@printf '%s\n' 'Nouveau ? -> QUICKSTART.md (de zero a ton ecosysteme sur Proxmox)'
|
|
@printf '%s\n' 'Flux: editer le plan -> make instancier -> make instancier-appliquer -> make deployer'
|
|
@printf '%s\n' ''
|
|
@printf '%s\n' 'VM'
|
|
@printf '%s\n' ' Creer une VM (VMID/IP/VLAN/passerelle lus dans le plan):'
|
|
@printf '%s\n' ' make creer-vm HOTE=web-frontal-01'
|
|
@printf '%s\n' ' Cloner seulement, sans passer par le plan:'
|
|
@printf '%s\n' ' make cloner-vm HOTE=web-frontal-01 VMID=95301 VLAN=15 ADRESSE_IP=10.0.2.31 PASSERELLE=10.0.2.1'
|
|
@printf '%s\n' ' Configurer Proxmox et le Vault API (parametres: docs/config-proxmox.md):'
|
|
@printf '%s\n' ' make config'
|
|
@printf '%s\n' ''
|
|
@printf '%s\n' 'Hotes'
|
|
@printf '%s\n' ' Planifier/modifier un hote: editer le plan, puis regenerer:'
|
|
@printf '%s\n' ' editer instance/plan/serveurs.yml (ou la vue Serveurs du GUI)'
|
|
@printf '%s\n' ' make instancier-appliquer'
|
|
@printf '%s\n' ' Verifier un deploiement a blanc (dry-run):'
|
|
@printf '%s\n' ' make verifier-deploiement HOTE=web-frontal-01'
|
|
@printf '%s\n' ' Remettre un hote en conformite selon ses groupes:'
|
|
@printf '%s\n' ' make deployer HOTE=web-frontal-01'
|
|
@printf '%s\n' ' Afficher un hote:'
|
|
@printf '%s\n' ' make hote-afficher HOTE=web-frontal-01'
|
|
@printf '%s\n' ' Diagnostiquer:'
|
|
@printf '%s\n' ' make verifier-hote LIMITE=web-frontal-01'
|
|
@printf '%s\n' ''
|
|
@printf '%s\n' 'Groupes'
|
|
@printf '%s\n' ' Appliquer un groupe complet:'
|
|
@printf '%s\n' ' make deployer-groupe GROUPE=serveur_debian'
|
|
@printf '%s\n' ' Convention:'
|
|
@printf '%s\n' ' groupe serveur_debian -> playbooks/groupes/serveur_debian.yml'
|
|
@printf '%s\n' ''
|
|
@printf '%s\n' 'Ecosysteme complet (orchestrateur)'
|
|
@printf '%s\n' ' Ordre de deploiement (couches + graphe):'
|
|
@printf '%s\n' ' make site-verifier # valide la coherence couches/graphe'
|
|
@printf '%s\n' ' python3 scripts/orchestrer.py ordre'
|
|
@printf '%s\n' ' (Re)generer playbooks/site.yml ordonne:'
|
|
@printf '%s\n' ' make site'
|
|
@printf '%s\n' ' CONFIGURER la flotte existante, couche par couche (2b, VM deja creees):'
|
|
@printf '%s\n' ' make deployer-tout CONFIRMER=true # (MODE_CHECK=1 pour un essai a blanc idempotent)'
|
|
@printf '%s\n' ' CREER toutes les VM du plan (2a, clone Proxmox):'
|
|
@printf '%s\n' ' make flotte-creer CONFIRMER=true'
|
|
@printf '%s\n' ' RECONSTRUIRE from-zero = creer les VM PUIS deployer (2a+2b, VM inexistantes):'
|
|
@printf '%s\n' ' make reconstruire CONFIRMER=true # alias: make myDay CONFIRMER=true'
|
|
@printf '%s\n' ''
|
|
@printf '%s\n' 'Flux reseau (pare-feu / audit)'
|
|
@printf '%s\n' ' Matrice d audit + apercus nftables resolus (NON actives):'
|
|
@printf '%s\n' ' make flux # -> docs/registre-flux.md + instance/flux-genere/*.nft'
|
|
@printf '%s\n' ' make flux-verifier # valide schema + coherence de matrice'
|
|
@printf '%s\n' ''
|
|
@printf '%s\n' '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_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
|
|
|
|
# LA SONDE QUI RAPPORTE CE QUI DISTINGUE (2026-08-28). Trois faux diagnostics en une
|
|
# journee, tous dus a l'instrument : `curl` et `bash /dev/tcp` ecrasent « la machine
|
|
# n'existe pas », « une politique refuse » et « la frontiere se tait » dans le meme mot.
|
|
# L'INSEMINATION, CODIFIEE (2026-08-28). Elle a d'abord ete conduite A LA MAIN depuis le
|
|
# runner du site — donc hors du depot, donc sans preuve, ce que ce projet refuse.
|
|
#
|
|
# `TENANT=` PLUTOT QUE LE SYMLINK `instance`. Le runner du SITE materialise et amorce
|
|
# PLUSIEURS tenants ; pointer un lien global sur l'un d'eux le ferait se prendre pour ce
|
|
# tenant. Il en NOMME un par commande, et `SETOPS_INSTANCE` suffit a le dire — verifie le
|
|
# 2026-08-28. C'est aussi ce qui borne le couplage que `creer-vm` imposait en silence :
|
|
# rien ne disait sur quels tenants ce lien avait le droit de pointer, ni jusqu'a quand.
|
|
# `env -u SETOPS_INVENTAIRE` EST OBLIGATOIRE, ET UN GARDE-FOU L'A PROUVE.
|
|
#
|
|
# Le make parent EXPORTE SETOPS_INVENTAIRE, derive de l'instance montee. Sans le retirer,
|
|
# la resolution du tenant demande heritait de l'inventaire d'un AUTRE ecosysteme :
|
|
#
|
|
# REFUS : SETOPS_INVENTAIRE designe un inventaire HORS de l'instance demandee
|
|
#
|
|
# Le refus vient d'inventory_rules, pas d'ici — il a intercepte une insemination qui
|
|
# aurait vise le mauvais ecosysteme (2026-08-28). C'est exactement le genre d'erreur
|
|
# qu'un runner de site, qui sert plusieurs locataires, commettrait en silence.
|
|
.PHONY: inseminer
|
|
inseminer: ansible-runtime ## Le SITE amorce le runner d'un tenant — TENANT=<dossier> [HOTE=<nom>]
|
|
@set -e; \
|
|
if [[ -z "$(TENANT)" ]]; then \
|
|
printf '%s\n' 'Refus: relancer avec TENANT=<dossier frere>, ex. TENANT=OPS-Chezlepro.'; \
|
|
exit 2; \
|
|
fi; \
|
|
if [[ ! -d "../$(TENANT)/plan" ]]; then \
|
|
printf '%s\n' 'Refus: ../$(TENANT)/plan introuvable — ce n est pas un ecosysteme.'; \
|
|
exit 2; \
|
|
fi; \
|
|
inv="$$(env -u SETOPS_INVENTAIRE SETOPS_INSTANCE=../$(TENANT) python3 -c 'import sys; sys.path.insert(0,"scripts"); from inventory_rules import inventaire_de; print(inventaire_de() or "")')"; \
|
|
if [[ -z "$$inv" || ! -f "$$inv" ]]; then \
|
|
printf '%s\n' "Refus: aucun inventaire pour $(TENANT). Le generer chez lui (make instancier)."; \
|
|
exit 2; \
|
|
fi; \
|
|
hote="$(HOTE)"; \
|
|
if [[ -z "$$hote" ]]; then \
|
|
hote="$$(python3 -c 'import sys,yaml; d=yaml.safe_load(open(sys.argv[1])) or {}; p=[d.get("all",{})]; out=[]; \
|
|
[ (out.extend((n.get("children") or {}).get("serveur_ops_tenant",{}).get("hosts",{}) or {}), p.extend((n.get("children") or {}).values())) for n in iter(lambda: p.pop() if p else None, None) ]; print(" ".join(sorted(set(out))))' "$$inv")"; \
|
|
fi; \
|
|
if [[ -z "$$hote" ]]; then \
|
|
printf '%s\n' "Refus: $(TENANT) ne declare aucun hote portant serveur_ops_tenant."; \
|
|
printf '%s\n' "L insemination vise LE RUNNER d un tenant, jamais une machine ordinaire."; \
|
|
exit 2; \
|
|
fi; \
|
|
if (( $$(wc -w <<< "$$hote") > 1 )); then \
|
|
printf '%s\n' "Refus: plusieurs runners declares ($$hote). Nommer lequel avec HOTE=."; \
|
|
exit 2; \
|
|
fi; \
|
|
python3 scripts/verifier_cle_amorcage.py || exit 2; \
|
|
printf 'Insemination de %s chez %s — couches sans secret : %s\n\n' "$$hote" "$(TENANT)" "$(COUCHES_INSEMINATION)"; \
|
|
for groupe in $(COUCHES_INSEMINATION); do \
|
|
printf '=== %s ===\n' "$$groupe"; \
|
|
SETOPS_INSTANCE="../$(TENANT)" SETOPS_INVENTAIRE="$$inv" \
|
|
ansible-playbook -i "$$inv" "$(DOSSIER_PLAYBOOKS_GROUPES)/$$groupe.yml" --limit "$$hote"; \
|
|
done; \
|
|
printf '\n%s\n' "$$hote porte desormais le moteur, son plan et la carte de la fabric."; \
|
|
printf '%s\n' "IL N EST PAS ARME : ni voute, ni cle — le site ne les detient pas."; \
|
|
printf '%s\n' "L armer est le geste d un humain, depuis son poste :"; \
|
|
printf '%s\n' " make appliquer GROUPE=serveur_ops_tenant (voir docs/filiation-emancipation.md)"
|
|
|
|
# PROUVER QU'UN LIEN DE FILIATION EST COUPE — la quatrieme ligne de
|
|
# `docs/filiation-emancipation.md`, celle qu'on oublie : « une emancipation non prouvee
|
|
# est une emancipation non faite ».
|
|
#
|
|
# GESTE DESTRUCTIF ET REVERSIBLE : il COUPE l'amont pendant quelques secondes pour voir
|
|
# si la fonction tient sans lui. Sonder ne prouverait rien — tant que l'amont repond, une
|
|
# fonction qui marche ne dit pas d'ou vient l'octet.
|
|
#
|
|
# LE MEME ESSAI REND LES DEUX VERDICTS, et c'est voulu : « emancipe, prouve » quand la
|
|
# fonction tient, « pas emancipe, dependance REELLE » quand elle casse. Le second est le
|
|
# controle negatif du premier.
|
|
.PHONY: emancipation-prouver
|
|
emancipation-prouver: ansible-runtime _instance-requise ## Prouve qu un lien est coupe — SERVICE=<artefacts|genome|resolveur> [HOTE=] CONFIRMER=true
|
|
@set -e; \
|
|
if [[ -z "$(SERVICE)" ]]; then \
|
|
printf '%s\n' 'Refus: relancer avec SERVICE=<artefacts|genome|resolveur>.'; \
|
|
exit 2; \
|
|
fi; \
|
|
if [[ "$(CONFIRMER)" != "true" ]]; then \
|
|
printf '%s\n' 'Refus: cette preuve COUPE l amont quelques secondes pour mesurer.'; \
|
|
printf '%s\n' 'La coupure est retiree quoi qu il arrive (bloc `always`), mais'; \
|
|
printf '%s\n' 'pendant ce temps la fonction eprouvee peut echouer sur cet hote.'; \
|
|
printf '%s\n' 'Relancer avec CONFIRMER=true.'; \
|
|
exit 2; \
|
|
fi; \
|
|
ansible-playbook -i $(INVENTAIRE_PRODUCTION) playbooks/maintenance/emancipation-prouver.yml \
|
|
-e emancipation_service=$(SERVICE) \
|
|
$(if $(HOTE),-e emancipation_hotes=$(HOTE)) $(ARGS)
|
|
|
|
.PHONY: sonder
|
|
sonder: ## Sonde une cible et DIT ce qui distingue — CIBLE=<hote|ip> [PORTS="22 443"]
|
|
@if [[ -z "$(CIBLE)" ]]; then \
|
|
printf '%s\n' 'Refus: relancer avec CIBLE=<hote du plan, ou adresse>.'; \
|
|
printf '%s\n' 'Ex. make sonder CIBLE=infra-dns-01 PORTS="53 22"'; \
|
|
exit 2; \
|
|
fi
|
|
@python3 scripts/sonder.py $(CIBLE) $(or $(PORTS),22) $(if $(DELAI),--delai $(DELAI))
|
|
|
|
.PHONY: verifier
|
|
verifier: lint test inventaire-verifier site-verifier flux-verifier syntaxe ## Rejoue les preuves SANS reecrire le rapport (verification rapide)
|
|
python3 scripts/prouver.py --verifier
|
|
|
|
# Ce que la CI execute, et que tu peux rejouer a l'identique.
|
|
#
|
|
# Le modele PUBLIC est monte comme instance jetable et vise par SETOPS_INSTANCE /
|
|
# SETOPS_UNDERLAY : AUCUN symlink n'est touche, ton instance reste montee pendant
|
|
# l'execution. Deux details ne sont pas negociables, mesures le 2026-08-20 :
|
|
#
|
|
# - l'instance jetable est un DOSSIER FRERE du depot, pas un /tmp. La federation se
|
|
# decouvre par les dossiers freres (`devis_reseau.decouvrir`) : ailleurs, elle est
|
|
# invisible, et quatre preuves tombent en disant « aucun tenant federe decouvert » ;
|
|
# - `env -u SETOPS_INVENTAIRE`, sinon la variable exportee par CE make survit dans le
|
|
# sous-make et designe l'inventaire de l'instance ACTIVE — le piege de P03.
|
|
#
|
|
# Et SETOPS_UNDERLAY n'est pose QUE pour la verification, jamais pour l'application :
|
|
# l'inventaire de l'instance jetable doit etre ecrit avec la meme fabric que celle avec
|
|
# laquelle P03 le REGENERERA ensuite. En posant l'underlay du modele des l'application,
|
|
# `make ci` passait sur un depot nu et echouait chez un exploitant dont le symlink
|
|
# `underlay.yml` designe une autre fabric — quatre hotes « en ecart », pour un ecart
|
|
# fabrique par la commande elle-meme.
|
|
# LE GENOME : les depots sans lesquels cet ecosysteme ne peut pas etre refabrique, et la
|
|
# PARENTE qu'il inscrit (de quel moteur il descend, a quel commit, sous quelle etiquette
|
|
# signee). Perdre les VM coute du temps ; perdre ces depots coute l'ecosysteme.
|
|
.PHONY: genome genome-inscrire genome-verifier
|
|
genome: ## Les depots requis a la reproduction de cet ecosysteme, et leur etat
|
|
python3 scripts/genome.py lister
|
|
|
|
genome-inscrire: ## Inscrit la parente dans l'instance (instance/parente.yml)
|
|
python3 scripts/genome.py inscrire
|
|
|
|
genome-verifier: ## La parente inscrite tient-elle encore ? (code de sortie)
|
|
python3 scripts/genome.py verifier
|
|
|
|
.PHONY: ci
|
|
ci: ## Verifie le depot comme la CI : modele public monte a l'ecart, sans toucher ton instance
|
|
@set -e ; \
|
|
frere="../instance-ci-$$$$" ; \
|
|
trap 'rm -rf "$$frere"' EXIT ; \
|
|
rm -rf "$$frere" ; cp -r exemples/modeles/socle "$$frere" ; \
|
|
env -u SETOPS_INVENTAIRE SETOPS_INSTANCE="$$frere" \
|
|
$(MAKE) --no-print-directory instancier-appliquer FORCE=1 ; \
|
|
env -u SETOPS_INVENTAIRE SETOPS_INSTANCE="$$frere" \
|
|
SETOPS_UNDERLAY="$$frere/underlay.yml" \
|
|
$(MAKE) --no-print-directory verifier
|
|
|
|
# Harnais de preuve : rejoue les preuves automatisables du registre et ecrit
|
|
# docs/audit/preuve-<date>.md (piece justificative horodatee, rejouable).
|
|
# `make verifier` l'appelle en mode --verifier (preuves seules, aucun rapport ecrit).
|
|
.PHONY: prouver
|
|
prouver: ansible-runtime _instance-requise ## Execute les preuves et ecrit docs/audit/preuve-<date>.md
|
|
python3 scripts/prouver.py
|
|
|
|
.PHONY: inventaire hote-planifier hote-ajouter hote-groupes hote-afficher appliquer deployer deployer-groupe cloner-vm creer-vm config inventaire-ui inventaire-verifier inventaire-lister inventaire-graphe inventaire-hote inventaire-lab inventaire-production instance-utiliser instance-courante
|
|
inventaire: inventaire-production ## Verifie l'inventaire et en affiche le graphe (lab puis production)
|
|
|
|
# Bascule le symlink 'instance' vers un autre dépôt d'instance (séparation par
|
|
# instance : prod vs bac à sable). Ex. : make instance-utiliser NOM=OPS-Chezlepro-lab
|
|
instance-utiliser: ## Bascule l'instance active (symlink instance/) vers un dossier frere — NOM=<dossier>
|
|
@if [[ -z "$(NOM)" ]]; then printf '%s\n' "Usage: make instance-utiliser NOM=<dossier-frère> (ex. OPS-Chezlepro-lab)"; exit 2; fi
|
|
@if [[ ! -d "../$(NOM)" ]]; then printf '%s\n' "Introuvable: ../$(NOM)"; exit 2; fi
|
|
@if [[ -e instance && ! -L instance ]]; then printf '%s\n' "Refus: 'instance' existe et n'est pas un symlink."; exit 2; fi
|
|
@rm -f instance && ln -s "../$(NOM)" instance
|
|
@printf 'instance -> %s\n' "$$(readlink instance)"
|
|
|
|
instance-courante: ## Affiche vers quel ecosysteme pointe l'instance active
|
|
@printf 'instance -> %s\n' "$$(readlink instance 2>/dev/null || echo '(non monté)')"
|
|
|
|
# Vue d'ensemble : toutes les instances de la fédération, l'active (*), leur index,
|
|
# plage VLAN, statut fédéré/prod ; signale les collisions d'index. Lecture seule.
|
|
instances: ## Liste les ecosystemes decouverts (dossiers freres) et signale les collisions d'index
|
|
@python3 scripts/instances.py
|
|
|
|
# (Re)génère le plan de recette (docs/audit/plan-de-recette.md) depuis les exercices
|
|
# du wiki. La preuve P22 vérifie qu'il reste à jour.
|
|
plan-recette: ## Regenere docs/audit/plan-de-recette.md depuis le wiki
|
|
@python3 scripts/plan_recette.py
|
|
|
|
# Liste les modèles disponibles (socle + SETOPS_MODELES) pour créer une instance.
|
|
instance-modeles: ## Liste les modeles d'ecosysteme disponibles
|
|
@python3 scripts/instance_creer.py --lister-modeles
|
|
|
|
# Crée un dépôt d'instance frère depuis un modèle. Ne bascule pas le symlink.
|
|
# Ex. : make instance-creer NOM=OPS-ClientX MODELE=socle INDEX=4
|
|
instance-creer: ## Cree un nouvel ecosysteme depuis un modele — NOM=<nom> MODELE=<modele>
|
|
@python3 scripts/instance_creer.py --nom "$(NOM)" --modele "$(MODELE)" \
|
|
$(if $(INDEX),--index $(INDEX),)
|
|
|
|
# Crée un MODÈLE (dépôt privé). Deux modes :
|
|
# base : copier un modèle générique -> make model-creer MODE=base BASE=identite NOM=maison-obnl
|
|
# instance : promouvoir une instance -> make model-creer MODE=instance SOURCE=OPS-Chezlepro NOM=cabinet
|
|
model-creer: ## Cree un modele d'ecosysteme — MODE=<mode> NOM=<nom>
|
|
@python3 scripts/model_creer.py --mode "$(MODE)" --nom "$(NOM)" \
|
|
$(if $(BASE),--base $(BASE),) $(if $(SOURCE),--source $(SOURCE),) $(if $(DEST),--dest $(DEST),)
|
|
|
|
config: ## Affiche la configuration Proxmox lue par le moteur
|
|
python3 scripts/config_proxmox.py
|
|
|
|
inventaire-ui: _instance-requise ## Ouvre la console d'exploitation (GUI web) sur l'inventaire actif
|
|
python3 scripts/inventory_gui.py --inventaire $(FICHIER_INVENTAIRE)
|
|
|
|
hote-ajouter hote-planifier hote-groupes:
|
|
@printf '%s\n' 'Cible depreciee: l inventaire est GENERE depuis le plan, il ne s edite plus a la main.'
|
|
@printf '%s\n' 'Declare ou modifie l hote dans instance/plan/serveurs.yml (ou la vue Serveurs du GUI), puis :'
|
|
@printf '%s\n' ' make instancier-appliquer'
|
|
@printf '%s\n' '(creer-vm lit desormais VMID/IP/VLAN/passerelle directement dans l inventaire genere.)'
|
|
@exit 2
|
|
|
|
hote-afficher: ansible-runtime ## Affiche tout ce que le plan derive pour un hote — HOTE=<nom>
|
|
@if [[ -z "$(HOTE)" ]]; then \
|
|
printf '%s\n' 'Refus: relancer avec HOTE=nom_hote.'; \
|
|
exit 2; \
|
|
fi
|
|
python3 scripts/inventory_host.py --inventaire $(FICHIER_INVENTAIRE) afficher --hote $(HOTE)
|
|
|
|
appliquer: ansible-runtime ## Applique un groupe a la flotte — GROUPE=<groupe>
|
|
@if [[ -z "$(GROUPE)" ]]; then \
|
|
printf '%s\n' 'Refus: relancer avec GROUPE=nom_groupe.'; \
|
|
exit 2; \
|
|
fi
|
|
@if [[ ! -f "$(DOSSIER_PLAYBOOKS_GROUPES)/$(GROUPE).yml" ]]; then \
|
|
printf '%s\n' 'Refus: aucun playbook pour ce groupe: $(DOSSIER_PLAYBOOKS_GROUPES)/$(GROUPE).yml'; \
|
|
exit 2; \
|
|
fi
|
|
python3 scripts/inventory_host.py --inventaire $(INVENTAIRE_PRODUCTION) --dependances $(FICHIER_DEPENDANCES) verifier-dependances-groupe --groupe $(GROUPE)
|
|
ansible-playbook -i $(INVENTAIRE_PRODUCTION) "$(DOSSIER_PLAYBOOKS_GROUPES)/$(GROUPE).yml" --limit '$(GROUPE):&$(GROUPE_HOTES_ACTIFS)'
|
|
|
|
# ATTENDRE PLUTOT QUE CONSTATER (2026-08-27). `deployer` faisait un `ping` unique
|
|
# (`_verifier-acces-hote`). Depuis que `creer-vm` ne guette plus le SSH — il prouve la
|
|
# materialisation par l'agent invite, seul geste que le runner du SITE ait le droit de
|
|
# faire chez un tenant — c'est ICI qu'il faut attendre : enchainer `creer-vm` puis
|
|
# `deployer` tomberait sinon sur une machine encore en cloud-init. On y gagne au passage
|
|
# ce que le `ping` unique ne faisait pas : attendre que `dpkg` se libere, faute de quoi
|
|
# la premiere couche echoue sur un verrou APT.
|
|
#
|
|
# CONTREPARTIE ASSUMEE : un nom d'hote errone ne fait plus echouer tout de suite, il
|
|
# patiente ATTENTE_HOTE secondes. Echouer vite sur l'injoignable interdirait de chainer
|
|
# la creation au deploiement — le geste central d'une reconstruction.
|
|
deployer: _instance-requise ## Deploie un hote, couche par couche, dans l'ordre du graphe — HOTE=<nom>
|
|
@set -e; \
|
|
if [[ -z "$(HOTE)" ]]; then \
|
|
printf '%s\n' 'Refus: relancer avec HOTE=nom_hote.'; \
|
|
exit 2; \
|
|
fi; \
|
|
python3 scripts/inventory_host.py --inventaire $(FICHIER_INVENTAIRE) verifier-actif --hote $(HOTE); \
|
|
python3 scripts/inventory_host.py --inventaire $(FICHIER_INVENTAIRE) --dependances $(FICHIER_DEPENDANCES) verifier-dependances-hote --hote $(HOTE); \
|
|
playbooks="$$(python3 scripts/inventory_host.py --inventaire $(FICHIER_INVENTAIRE) playbooks --hote $(HOTE) --dossier-playbooks $(DOSSIER_PLAYBOOKS_GROUPES))"; \
|
|
if [[ -z "$$playbooks" ]]; then \
|
|
printf '%s\n' 'Refus: aucun playbook applicable pour HOTE=$(HOTE).'; \
|
|
exit 2; \
|
|
fi; \
|
|
$(VAULT_UNE_FOIS) \
|
|
$(MAKE) --no-print-directory _attendre-hote LIMITE="$(HOTE)"; \
|
|
$(MAKE) _verifier-privileges-hote LIMITE="$(HOTE)"; \
|
|
for playbook in $$playbooks; do \
|
|
ansible-playbook -i $(INVENTAIRE_PRODUCTION) "$$playbook" --limit "$(HOTE)"; \
|
|
done; \
|
|
$(MAKE) verifier-hote LIMITE="$(HOTE)"
|
|
|
|
.PHONY: site site-verifier deployer-tout
|
|
site: ansible-runtime ## Regenere playbooks/site.yml depuis les couches et le graphe de dependances
|
|
python3 scripts/orchestrer.py ecrire
|
|
ansible-playbook -i $(INVENTAIRE_PRODUCTION) playbooks/site.yml --syntax-check
|
|
|
|
site-verifier: ## Verifie que playbooks/site.yml correspond aux couches declarees
|
|
python3 scripts/orchestrer.py verifier
|
|
|
|
.PHONY: flux flux-verifier
|
|
cles-recenser: ## Montre ce qui n'existe QUE sur ce poste (sans rien ecrire)
|
|
python3 scripts/exporter_cles.py --recenser
|
|
|
|
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)
|
|
python3 scripts/devis_placement.py
|
|
|
|
sdn-plan: ansible-runtime ## Ecart entre le SDN EVPN (+ sortie des VRF) et son devis (aucune ecriture)
|
|
python3 scripts/appliquer_sdn.py
|
|
|
|
sdn-appliquer: ansible-runtime ## Reconcilie le SDN : cree ce qui manque, RETIRE ce qui est perime. CONFIRMER=true
|
|
@if [[ "$(CONFIRMER)" != "true" ]]; then \
|
|
printf '%s\n' "Refus: cette cible ECRIT sur le cluster (zones, VNets, sous-reseaux) et sur" \
|
|
"les noeuds de sortie (/etc/frr/frr.conf.local), et RETIRE ce qui est perime." \
|
|
"Relire d'abord 'make sdn-plan', puis: make sdn-appliquer CONFIRMER=true"; \
|
|
exit 2; fi
|
|
CONFIRMER=true python3 scripts/appliquer_sdn.py
|
|
|
|
.PHONY: ca-racine
|
|
# Ou deposer la racine. Par defaut le repertoire courant : c'est un certificat
|
|
# PUBLIC, pas un secret — il n'a rien a faire dans la voute, et tout a faire dans
|
|
# le magasin de confiance de qui administre.
|
|
CA_RACINE_DEST ?= ./root_ca.crt
|
|
|
|
ca-racine: ansible-runtime _instance-requise ## Recupere la racine de l'AC interne + son empreinte (a verifier AVANT de l'installer)
|
|
@set -e; \
|
|
hote="$$(python3 -c "import yaml,sys; d=yaml.safe_load(open('$(FICHIER_INVENTAIRE)')); \
|
|
import itertools; \
|
|
g=d['all']['children']; \
|
|
print(next(iter((g.get('serveur_step_ca') or {}).get('hosts') or {}), ''))" 2>/dev/null || true)"; \
|
|
if [[ -z "$$hote" ]]; then \
|
|
printf '%s\n' "Refus: aucun hote ne porte 'serveur_step_ca' dans $(FICHIER_INVENTAIRE)." \
|
|
"Cette instance n'a pas d'autorite de certification interne."; exit 2; fi; \
|
|
ansible -i $(FICHIER_INVENTAIRE) "$$hote" --become \
|
|
-m fetch -a "src=/etc/step-ca/certs/root_ca.crt dest=$(CA_RACINE_DEST) flat=yes" >/dev/null; \
|
|
printf '%s\n' "Racine de l'AC ecrite dans $(CA_RACINE_DEST) (depuis $$hote)."; \
|
|
printf '%s\n' "" " sujet : $$(openssl x509 -in $(CA_RACINE_DEST) -noout -subject | sed 's/^subject=//')"; \
|
|
printf '%s\n' " valide : $$(openssl x509 -in $(CA_RACINE_DEST) -noout -enddate | sed 's/^notAfter=//')"; \
|
|
printf '%s\n' " empreinte: $$(openssl x509 -in $(CA_RACINE_DEST) -noout -fingerprint -sha256 | sed 's/^.*=//' | tr -d ':' | tr 'A-Z' 'a-z')"; \
|
|
printf '%s\n' "" \
|
|
"COMPARE l'empreinte avec celle de l'AC avant de l'installer :" \
|
|
" make ca-empreinte" \
|
|
"" \
|
|
"Installer une AC, c'est lui donner le droit de signer N'IMPORTE QUEL nom" \
|
|
"pour ton navigateur. La comparaison est ce qui distingue ta racine d'une" \
|
|
"racine interceptee — ce n'est pas une formalite." \
|
|
"" \
|
|
" sudo cp $(CA_RACINE_DEST) /usr/local/share/ca-certificates/setops-root.crt" \
|
|
" sudo update-ca-certificates" \
|
|
" (Firefox a son propre magasin : Parametres > Certificats > Autorites)"
|
|
|
|
.PHONY: ca-empreinte
|
|
ca-empreinte: ansible-runtime _instance-requise ## Empreinte de la racine, lue SUR l'AC (le temoin de comparaison)
|
|
@set -e; \
|
|
hote="$$(python3 -c "import yaml; d=yaml.safe_load(open('$(FICHIER_INVENTAIRE)')); \
|
|
g=d['all']['children']; \
|
|
print(next(iter((g.get('serveur_step_ca') or {}).get('hosts') or {}), ''))" 2>/dev/null || true)"; \
|
|
if [[ -z "$$hote" ]]; then \
|
|
printf '%s\n' "Refus: aucun hote ne porte 'serveur_step_ca'."; exit 2; fi; \
|
|
ansible -i $(FICHIER_INVENTAIRE) "$$hote" --become -m command \
|
|
-a "step certificate fingerprint /etc/step-ca/certs/root_ca.crt" 2>/dev/null \
|
|
| tail -1 | tr -d ' \r'
|
|
|
|
.PHONY: frontiere-plan frontiere-appliquer frontiere-mesurer mtu-mesurer versions-mesurer
|
|
courriel-plan: ansible-runtime ## Chaine Postfix -> LDAP -> Dovecot -> IMAP (aucune ecriture)
|
|
@rm -f $(SETOPS_INSTANCE)/devis-courriel.json.*
|
|
@ansible-playbook -i $(SETOPS_INVENTAIRE) playbooks/maintenance/devis-courriel.yml >/dev/null
|
|
@python3 scripts/devis_courriel.py
|
|
|
|
postgresql-plan: ansible-runtime ## Chiffrement impose et portee des acces PostgreSQL (aucune ecriture)
|
|
@rm -f $(SETOPS_INSTANCE)/devis-postgresql.json.*
|
|
@ansible-playbook -i $(SETOPS_INVENTAIRE) playbooks/maintenance/devis-postgresql.yml >/dev/null
|
|
@python3 scripts/devis_postgresql.py
|
|
|
|
expositions-plan: ansible-runtime ## Chaque exposition du plan repond-elle ? (edge et poste, aucune ecriture)
|
|
@ansible-playbook -i $(SETOPS_INVENTAIRE) playbooks/maintenance/devis-expositions.yml >/dev/null
|
|
@python3 scripts/devis_expositions.py
|
|
|
|
versions-mesurer: ## De combien nos epinglages ont-ils vieilli ? (interroge les amonts, aucune ecriture)
|
|
@python3 scripts/devis_versions.py
|
|
|
|
mtu-mesurer: ansible-runtime ## L'invite porte-t-il le MTU de sa zone SDN ? (aucune ecriture)
|
|
@ansible-playbook -i $(SETOPS_INVENTAIRE) playbooks/maintenance/devis-mtu.yml >/dev/null
|
|
@python3 scripts/devis_mtu.py
|
|
|
|
frontiere-mesurer: ansible-runtime ## La frontiere refuse-t-elle ce qui n'est pas declare ? (sonde reelle, aucune ecriture)
|
|
@ansible-playbook -i $(SETOPS_INVENTAIRE) playbooks/maintenance/devis-frontiere.yml >/dev/null
|
|
@python3 scripts/devis_frontiere.py
|
|
|
|
certificats-plan: ansible-runtime ## Ecart entre les certificats sur disque et ceux reellement servis (aucune ecriture)
|
|
@rm -f $(SETOPS_INSTANCE)/devis-certificats.json.*
|
|
@ansible-playbook -i $(SETOPS_INVENTAIRE) playbooks/maintenance/devis-certificats.yml >/dev/null
|
|
@python3 scripts/devis_certificats.py
|
|
|
|
ports-verifier: ## Deux roles co-localises revendiquent-ils le meme port ? (lecture seule)
|
|
@python3 scripts/verifier_ports.py
|
|
|
|
intrants-verifier: ## Tout intrant qu'un role EXIGE est-il fourni par l'instance ? (lecture seule)
|
|
@python3 scripts/verifier_intrants.py
|
|
|
|
identite-plan: ansible-runtime ## Ecart entre l'identite deployee et ce que le plan derive (aucune ecriture)
|
|
@ansible-playbook -i $(SETOPS_INVENTAIRE) playbooks/maintenance/devis-identite.yml >/dev/null
|
|
@python3 scripts/devis_identite.py
|
|
|
|
frontiere-plan: ansible-runtime ## Ecart entre la frontiere OPNsense et son devis (aucune ecriture)
|
|
python3 scripts/appliquer_opnsense.py
|
|
|
|
frontiere-appliquer: ansible-runtime ## Reconcilie la frontiere : cree ce qui manque, RETIRE ce qui est perime. CONFIRMER=true
|
|
@if [[ "$(CONFIRMER)" != "true" ]]; then \
|
|
printf '%s\n' "Refus: cette cible ECRIT sur le pare-feu de bordure et RETIRE les regles perimees." \
|
|
"Relire d'abord 'make frontiere-plan', puis: make frontiere-appliquer CONFIRMER=true"; \
|
|
exit 2; fi
|
|
CONFIRMER=true python3 scripts/appliquer_opnsense.py
|
|
|
|
.PHONY: devis-proxmox-fw devis-proxmox-fw-verifier
|
|
devis-proxmox-fw: ansible-runtime ## Devis pare-feu Proxmox (est-ouest intra-tenant), derive du registre des flux
|
|
python3 scripts/devis_proxmox_fw.py $(if $(JSON),--json,)
|
|
|
|
devis-proxmox-fw-verifier: ## Verifie le devis du pare-feu est-ouest Proxmox (aucune ecriture)
|
|
python3 scripts/devis_proxmox_fw.py --verifier
|
|
|
|
.PHONY: site-intrants site-intrants-verifier
|
|
site-intrants: ## Les intrants que le SITE monte expose a ses locataires (derives)
|
|
python3 scripts/site_intrants.py $(if $(JSON),--json,)
|
|
|
|
site-intrants-verifier: ## Le locataire monte suit-il encore son site ? (aucune ecriture)
|
|
python3 scripts/site_intrants.py --verifier
|
|
|
|
.PHONY: 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: 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
|
|
|
|
|
|
flotte-creer: _instance-requise ## Cree les VM manquantes de la flotte depuis le plan — PARALLELE=n
|
|
@set -e; \
|
|
if [[ "$(CONFIRMER)" != "true" ]]; then \
|
|
printf '%s\n' 'Refus: creation de TOUTES les VM actives du plan (clone Proxmox).'; \
|
|
printf '%s\n' 'Relancer avec CONFIRMER=true.'; \
|
|
exit 2; \
|
|
fi; \
|
|
hotes="$$(python3 scripts/inventory_host.py --inventaire $(INVENTAIRE_PRODUCTION) lister-actifs)"; \
|
|
if [[ -z "$$hotes" ]]; then printf '%s\n' 'Refus: aucun hote actif dans le plan.'; exit 2; fi; \
|
|
par="$(PARALLELE)"; par="$${par:-4}"; \
|
|
tmp="$$(mktemp -d)"; trap 'rm -rf "$$tmp"' EXIT; \
|
|
$(VAULT_UNE_FOIS) \
|
|
printf 'Creation de %s VM, %s a la fois.\n' "$$(echo $$hotes | wc -w)" "$$par"; \
|
|
n=0; \
|
|
for h in $$hotes; do \
|
|
printf '\n=== Creation VM: %s ===\n' "$$h"; \
|
|
( $(MAKE) creer-vm HOTE="$$h" > "$$tmp/$$h.log" 2>&1; printf '%s' "$$?" > "$$tmp/$$h.rc" ) & \
|
|
n=$$((n+1)); \
|
|
if (( n >= par )); then wait -n || true; n=$$((n-1)); fi; \
|
|
done; \
|
|
wait; \
|
|
echec=0; \
|
|
for h in $$hotes; do \
|
|
printf '\n----- journal de %s -----\n' "$$h"; \
|
|
cat "$$tmp/$$h.log" 2>/dev/null || printf '(aucune sortie)\n'; \
|
|
rc="$$(cat "$$tmp/$$h.rc" 2>/dev/null || printf 1)"; \
|
|
if [[ "$$rc" != 0 ]]; then printf '!! %s a ECHOUE (code %s)\n' "$$h" "$$rc"; echec=1; fi; \
|
|
done; \
|
|
if (( echec )); then printf '\nAu moins une VM n a pas ete creee — rien ne continue.\n'; exit 2; fi; \
|
|
printf '\nToutes les VM actives sont creees.\n'
|
|
|
|
# --- LES MACHINES DU SITE -----------------------------------------------------
|
|
#
|
|
# UN SITE N'EST PAS UN PLAN. Un tenant se derive de son `index` ; un site n'en a pas et
|
|
# ne derive de rien — il EST le terrain sur lequel les tenants derivent. Ses machines se
|
|
# declarent dans `underlay.yml`, a cote des switches et des hyperviseurs qui les portent.
|
|
#
|
|
# D'ou des cibles distinctes de `flotte-*` : elles ne demandent AUCUNE instance montee.
|
|
# Le site existe avant tout tenant, et doit pouvoir naitre quand aucun n'est encore la.
|
|
#
|
|
# Le clone lui-meme reste `cloner-vm` — le seul chemin eprouve, et il n'y en aura pas
|
|
# deux. `site_machines.py` ne fait que traduire la declaration en ses parametres.
|
|
.PHONY: site-decrire
|
|
site-decrire: ## Les machines de l'hebergeur declarees dans l'underlay
|
|
@python3 scripts/site_machines.py decrire
|
|
|
|
.PHONY: site-inventaire
|
|
site-inventaire: ## L'inventaire dynamique du site, tel qu'Ansible le voit
|
|
@ansible-inventory -i scripts/site_inventaire.py --graph
|
|
|
|
.PHONY: site-appliquer
|
|
site-appliquer: ## Applique un role aux machines du site — GROUPE=<serveur_ops_site|...>
|
|
@# `GROUPE` a un defaut global (GROUPE_DEBIAN) : tester s'il est VIDE ne protege de
|
|
@# rien, ce garde-fou-la ne pourrait jamais se declencher. Le vrai risque, observe :
|
|
@# lancer un playbook de TENANT contre l'inventaire du SITE. Ansible n'y trouve aucun
|
|
@# hote, n'a rien a faire, et sort avec 0 — un succes qui n'a rien fait. On refuse donc
|
|
@# tout groupe qui n'existe pas DANS CET INVENTAIRE-CI.
|
|
@#
|
|
@# LA VOUTE DU SITE est derivee du symlink `underlay.yml`, jamais redeclaree. Les roles
|
|
@# partages exigent leurs secrets (Forgejo : secret_key, internal_token, admin_password).
|
|
@# Un tenant les tient de `group_vars/all/vault.yml` ; le site les tient de
|
|
@# `underlay.vault.yml`, qui vit a cote de sa carte et hors depot. Sans le `-e @`, le
|
|
@# role echoue sur son assertion — et l'echec ne dit pas qu'il manque un FICHIER,
|
|
@# seulement que les valeurs sont vides.
|
|
@set -e; \
|
|
if [[ ! -f "playbooks/groupes/$(GROUPE).yml" ]]; then \
|
|
printf 'Refus: aucun playbook pour le groupe %s.\n' "$(GROUPE)"; exit 2; \
|
|
fi; \
|
|
connus="$$(python3 scripts/site_inventaire.py --list | python3 -c \
|
|
'import json,sys; print(" ".join(k for k in json.load(sys.stdin) if k != "_meta"))')"; \
|
|
if [[ " $$connus " != *" $(GROUPE) "* ]]; then \
|
|
printf 'Refus: le groupe %s n existe pas au site.\n' "$(GROUPE)"; \
|
|
printf 'Groupes du site : %s\n' "$$connus"; \
|
|
exit 2; \
|
|
fi; \
|
|
voute="$$(dirname "$$(readlink -f underlay.yml)")/underlay.vault.yml"; \
|
|
supp=(); [[ -f "$$voute" ]] && supp+=( -e "@$$voute" ); \
|
|
ansible-playbook -i scripts/site_inventaire.py "playbooks/groupes/$(GROUPE).yml" \
|
|
"$${supp[@]}" $(ARGS)
|
|
|
|
.PHONY: fiches
|
|
fiches: ## Regenere la fiche de chaque role (docs/roles/) — ROLE=<nom> pour une seule
|
|
@# CHAQUE ROLE DECLARE DEJA CE QU'IL FAUDRAIT DESSINER. La fiche ne fait que le lire :
|
|
@# `meta/flux.yml` (qui lui parle), `supervision.yml` (quel verdict il rend),
|
|
@# `metriques.yml` (quelles series il expose), `empreinte.yml` (ce qu'il coute),
|
|
@# `authentification.yml` (qui entre). Chaque ligne porte la RAISON declaree.
|
|
@#
|
|
@# GENEREES, JAMAIS ECRITES. Soixante-huit pages faites a la main seraient perimees
|
|
@# avant la fin du mois — c'est « une liste qui suit une autre prend du retard », et
|
|
@# une soixante-neuvieme liste ne ferait pas exception.
|
|
@#
|
|
@# UNE SECTION VIDE EST UNE INFORMATION : un role sans sonde l'affiche, et l'ensemble
|
|
@# devient la carte de ce qui reste a faire, tenue a jour toute seule.
|
|
python3 scripts/fiche_role.py $(ROLE)
|
|
|
|
.PHONY: site-deployer-tout
|
|
site-deployer-tout: ansible-runtime ## Deploie TOUT le site dans l'ordre des couches — CONFIRMER=true
|
|
@# LE SITE AVAIT L'ORCHESTRATION, PAS LE MOYEN DE LA LANCER (2026-09-14).
|
|
@#
|
|
@# `playbooks/site.yml` est genere par `orchestrer.py` et ordonne les couches pour
|
|
@# N'IMPORTE QUELLE instance — son en-tete le dit : « le meme fichier sert toute
|
|
@# instance ». Mais `deployer-tout` ne vise que l'inventaire d'un LOCATAIRE, et le
|
|
@# site n'avait que `site-appliquer GROUPE=<un seul>`.
|
|
@#
|
|
@# CE QUE CA COUTAIT : le site ne se DEPLOYAIT que groupe par groupe, a la main, dans
|
|
@# un ordre qu'il fallait se rappeler. Un hebergeur qu'on ne peut remonter qu'en
|
|
@# enchainant onze groupes de memoire n'est pas reconstructible — il est reparable par
|
|
@# quelqu'un qui se souvient. C'est nommement l'une des trois limites du jalon de
|
|
@# reconstruction autonome : « le SITE jamais reconstruit ».
|
|
@#
|
|
@# TROIS DIFFERENCES AVEC `deployer-tout`, ET AUCUNE N'EST COSMETIQUE :
|
|
@#
|
|
@# l'inventaire est un SCRIPT (`site_inventaire.py`), pas un fichier : un site se
|
|
@# derive de son underlay, il ne se fige pas dans un `hosts.yml`.
|
|
@# la voute vit a cote de la carte (`underlay.vault.yml`), hors depot, et non
|
|
@# dans `group_vars/all`. Sans le `-e @`, les roles partages echouent
|
|
@# sur une assertion qui ne dit pas qu'il manque un FICHIER.
|
|
@# le perimetre est `hotes_actifs`, comme chez un locataire — les hyperviseurs et
|
|
@# la frontiere sont dans l'inventaire mais ne se deploient pas.
|
|
@set -e; \
|
|
if [[ "$(CONFIRMER)" != "true" ]]; then \
|
|
printf '%s\n' 'Refus: deploiement ORCHESTRE de TOUT le site (action impactante).'; \
|
|
printf '%s\n' 'Relancer avec CONFIRMER=true. Astuce: tester d abord en idempotent avec MODE_CHECK=1.'; \
|
|
exit 2; \
|
|
fi; \
|
|
python3 scripts/verifier_genome_a_jour.py || exit 2; \
|
|
python3 scripts/orchestrer.py verifier; \
|
|
python3 scripts/orchestrer.py ecrire; \
|
|
voute="$$(dirname "$$(readlink -f underlay.yml)")/underlay.vault.yml"; \
|
|
supp=(); [[ -f "$$voute" ]] && supp+=( -e "@$$voute" ); \
|
|
mode="$$([[ -n "$(MODE_CHECK)" ]] && printf -- '--check --diff' || true)"; \
|
|
ansible-playbook -i scripts/site_inventaire.py playbooks/site.yml \
|
|
--limit "$(GROUPE_HOTES_ACTIFS)" "$${supp[@]}" $$mode $(ARGS)
|
|
|
|
# --- LE GENOME REMONTE SUR LA FORGE DU SITE -----------------------------------
|
|
#
|
|
# La forge du genome vit DANS le site, sur un reseau que le poste de l'exploitant ne route
|
|
# pas — et c'est voulu : le poste n'a de patte que sur l'administration, la frontiere
|
|
# police le reste. Il ne peut donc pas pousser lui-meme.
|
|
#
|
|
# On ne perce pas un chemin pour lui. Le RUNNER est dans le site, il porte le genome, il a
|
|
# une identite : on lui PORTE les commits dans un `git bundle` — un fichier, verifiable,
|
|
# qui ne demande aucun reseau — et c'est LUI qui pousse, avec SA cle.
|
|
#
|
|
# Mesure du 2026-08-26 : la forge du site etait restee quatre commits derriere le poste,
|
|
# sans que rien ne le signale. Le runner clonait donc un moteur perime — dont, ce jour-la,
|
|
# le correctif qui desarme le pare-feu Proxmox. Un ecosysteme qui se reproduit depuis une
|
|
# forge en retard reproduit ses defauts.
|
|
#
|
|
# CE QUI EST POUSSE SE DERIVE de `serveur_ops_depots` (au plan du site) : on pousse ceux
|
|
# dont une copie locale existe a cote du moteur, et rien d'autre.
|
|
# LE GABARIT PORTE-T-IL CE QUE LE SITE DECLARE DE LUI ?
|
|
#
|
|
# Son type de machine ne se corrige pas apres coup : convertir `i440fx` -> `q35` remplace
|
|
# le materiel sous un systeme qui croit connaitre le sien, et chaque panne qui s'ensuit
|
|
# ressemble a autre chose qu'a sa cause. On verifie donc la SOURCE — la propriete s'herite,
|
|
# tous les clones d'un gabarit conforme le sont.
|
|
#
|
|
# A LA DEMANDE, pas dans `make prouver` : ce controle exige le cluster, que le harnais ne
|
|
# suppose pas joignable. Meme nature que `genome-etat` et `underlay-plan`.
|
|
.PHONY: routes-fabric-etat
|
|
routes-fabric-etat: ansible-runtime ## Les hyperviseurs routent-ils toutes les zones du SITE ? — ne corrige rien
|
|
@# UNE LISTE QUI DOIT SUIVRE UNE AUTRE LISTE PREND DU RETARD. Les routes specifiques
|
|
@# de `vmbr0` s'arretaient a 10.0.34.0/24 quand le site en declarait six : les zones
|
|
@# `sauvegarde` et `supervision` etaient nees sans que les routes suivent. Le symptome
|
|
@# ne ressemblait pas a une route manquante — il ressemblait a un pare-feu.
|
|
python3 scripts/routes_fabric_etat.py
|
|
|
|
.PHONY: expositions-etat
|
|
expositions-etat: ## Chaque exposition est-elle servie sous un certificat qui la porte ? — ne corrige rien
|
|
@# `serveur_nginx` POSE LE VHOST ET LAISSE LE SAN EN ARRIERE. Mesure deux fois le
|
|
@# 2026-09-10, sur `grafana -> observatoire` puis `icinga -> vigie` : la zone publiait
|
|
@# le nouveau nom, le vhost repondait, le client OIDC l'acceptait — et le certificat
|
|
@# servi portait encore l'ancien. C'est `client_pki` qui reemet, et rien ne le reclame.
|
|
@# Ajouter `--site` pour l'ecosysteme du SITE.
|
|
python3 scripts/expositions_etat.py $(if $(SITE),--site)
|
|
|
|
.PHONY: gabarit-etat
|
|
gabarit-etat: ## Le gabarit porte-t-il ce que le SITE declare ? — ne corrige rien, regarde
|
|
python3 scripts/gabarit_etat.py
|
|
|
|
.PHONY: genome-etat
|
|
genome-etat: ## Ce que la forge du site porte, face au poste — ne corrige rien, regarde
|
|
@set -e; \
|
|
voute="$$(dirname "$$(readlink -f underlay.yml)")/underlay.vault.yml"; \
|
|
supp=(); [[ -f "$$voute" ]] && supp+=( -e "@$$voute" ); \
|
|
ansible-playbook -i scripts/site_inventaire.py \
|
|
playbooks/maintenance/genome_etat.yml "$${supp[@]}" $(ARGS)
|
|
|
|
.PHONY: genome-pousser
|
|
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-requise ## DESTRUCTIF : detruit les VM derivees du plan — exige CONFIRMER=true ET INSTANCE=<nom>
|
|
@python3 scripts/raser.py $(if $(INSTANCE),--instance $(INSTANCE)) $(if $(HOTE),--hote $(HOTE)) $(if $(filter true,$(CONFIRMER)),--confirmer)
|
|
|
|
# `make flux` D'ABORD, et ce n'est pas une precaution : sans lui, `instance/flux-genere/`
|
|
# est VIDE et le socle deploie nftables en `policy drop` SANS AUCUNE REGLE DERIVEE. La
|
|
# flotte se monte, SSH repond depuis l'administration — et tout le reste est mur.
|
|
#
|
|
# Invisible sur un ecosysteme deja construit, dont les .nft trainaient d'une execution
|
|
# precedente. Trouve le 2026-08-10 en montant Technolibre depuis zero : l'AC etait
|
|
# debout, son port 8443 en ecoute, et `step ca bootstrap` expirait depuis une VM du
|
|
# meme sous-reseau. Zero regle pour 8443 dans le ruleset, `flux-genere/` vide.
|
|
reconstruire: _instance-requise ## Reconstruit un ecosysteme depuis zero : VM puis deploiement complet
|
|
@set -e; \
|
|
if [[ "$(CONFIRMER)" != "true" ]]; then \
|
|
printf '%s\n' 'Refus: RECONSTRUCTION — cree les VM manquantes (2a) PUIS deploie tout (2b).'; \
|
|
printf '%s\n' 'Idempotent : une VM deja presente est sautee (clone par nom), le deploiement converge.'; \
|
|
printf '%s\n' 'Relancer avec CONFIRMER=true.'; \
|
|
exit 2; \
|
|
fi; \
|
|
$(MAKE) flux; \
|
|
$(MAKE) flotte-creer CONFIRMER=true; \
|
|
$(MAKE) _attendre-flotte; \
|
|
$(MAKE) _amorcer-socle; \
|
|
$(MAKE) deployer-tout CONFIRMER=true
|
|
|
|
# « Go ahead, make my day. » — LE bouton rouge : alias de reconstruire (Clint Eastwood).
|
|
# Cree toutes les VM puis deploie tout, en une commande. Garde CONFIRMER=true.
|
|
.PHONY: myDay
|
|
# « Une PKI et un DNS fonctionnels avant toute chose » — contrainte d'architecture du
|
|
# 2026-08-08. `deployer-tout` deroule par COUCHES : tous les hotes recoivent le socle,
|
|
# puis tous recoivent la couche suivante. C'est correct, mais ca laisse chaque VM
|
|
# reclamer un certificat a une autorite qui n'est pas encore debout — et l'echec se lit
|
|
# alors comme un defaut du role, pas comme un defaut d'ordre.
|
|
#
|
|
# On monte donc l'AC puis le DNS COMPLETEMENT d'abord, hote par hote (`make deployer`
|
|
# ordonne deja les groupes d'un hote par couches). Les deux se DERIVENT du plan :
|
|
# deplacer l'autorite deplace l'amorcage avec elle.
|
|
_amorcer-socle: ansible-runtime _instance-requise
|
|
@set -e; \
|
|
for h in $$(python3 scripts/socle_amorcage.py); do \
|
|
printf '\n=== Amorcage du socle : %s ===\n' "$$h"; \
|
|
$(MAKE) deployer HOTE="$$h"; \
|
|
done; \
|
|
printf '\nPKI et DNS debout : le reste de la flotte peut monter.\n'
|
|
|
|
myDay: reconstruire ## Alias strict de `reconstruire` (meme cible, memes gardes)
|
|
|
|
deployer-groupe: ## Deploie un seul groupe sur toute la flotte — GROUPE=<groupe>
|
|
@if [[ -z "$(GROUPE)" ]]; then \
|
|
printf '%s\n' 'Refus: relancer avec GROUPE=nom_groupe.'; \
|
|
exit 2; \
|
|
fi
|
|
$(MAKE) appliquer GROUPE="$(GROUPE)"
|
|
|
|
.PHONY: verifier-deploiement
|
|
verifier-deploiement: ansible-runtime ## Verifie l'etat de la flotte apres deploiement
|
|
@set -e; \
|
|
if [[ -z "$(HOTE)" ]]; then \
|
|
printf '%s\n' 'Refus: relancer avec HOTE=nom_hote.'; \
|
|
exit 2; \
|
|
fi; \
|
|
python3 scripts/inventory_host.py --inventaire $(FICHIER_INVENTAIRE) verifier-actif --hote $(HOTE); \
|
|
python3 scripts/inventory_host.py --inventaire $(FICHIER_INVENTAIRE) --dependances $(FICHIER_DEPENDANCES) verifier-dependances-hote --hote $(HOTE); \
|
|
playbooks="$$(python3 scripts/inventory_host.py --inventaire $(FICHIER_INVENTAIRE) playbooks --hote $(HOTE) --dossier-playbooks $(DOSSIER_PLAYBOOKS_GROUPES))"; \
|
|
if [[ -z "$$playbooks" ]]; then \
|
|
printf '%s\n' 'Refus: aucun playbook applicable pour HOTE=$(HOTE).'; \
|
|
exit 2; \
|
|
fi; \
|
|
vault_chiffre="$$(grep -rlsIF '$$ANSIBLE_VAULT' $(dir $(INVENTAIRE_PRODUCTION))group_vars 2>/dev/null | head -1 || true)"; \
|
|
if [[ -n "$$vault_chiffre" && -z "$${ANSIBLE_VAULT_PASSWORD_FILE:-}$${ANSIBLE_VAULT_IDENTITY_LIST:-}" ]]; then \
|
|
if [[ -t 0 ]]; then \
|
|
read -r -s -p 'Mot de passe du vault Ansible: ' mdp; echo; \
|
|
vf="$$(mktemp)"; printf '%s' "$$mdp" > "$$vf"; chmod 600 "$$vf"; \
|
|
export ANSIBLE_VAULT_PASSWORD_FILE="$$vf"; \
|
|
trap 'rm -f "$$vf"' EXIT; \
|
|
else \
|
|
printf '%s\n' 'Refus: vault chiffre detecte mais aucun mot de passe (entree non interactive). Fournir ANSIBLE_VAULT_PASSWORD_FILE ou le champ vault de la GUI.'; \
|
|
exit 2; \
|
|
fi; \
|
|
fi; \
|
|
for playbook in $$playbooks; do \
|
|
ansible-playbook -i $(INVENTAIRE_PRODUCTION) "$$playbook" --limit "$(HOTE)" --check --diff; \
|
|
done
|
|
|
|
cloner-vm: ansible-runtime ## Clone une VM depuis le gabarit dore — HOTE=<nom> VMID=<id>
|
|
@if [[ -z "$(HOTE)" || -z "$(VMID)" ]]; then \
|
|
printf '%s\n' 'Refus: relancer avec HOTE=nom VMID=id_clone.'; \
|
|
exit 2; \
|
|
fi
|
|
@# En SDN EVPN, l'etiquette est portee par le VNet : `instancier` emet donc un VLAN
|
|
@# VIDE et un pont derive (t17serv). Exiger un VLAN ici rejetait toute VM de tenant.
|
|
@# Un VLAN vide n'est accepte QUE si un pont est fourni — sinon la VM n'aurait ni
|
|
@# etiquette ni VNet, et se retrouverait branchee nulle part.
|
|
@if [[ -z "$(VLAN)" && -z "$(PONT_PROXMOX)" ]]; then \
|
|
printf '%s\n' 'Refus: relancer avec VLAN=id_vlan, ou PONT_PROXMOX=<vnet> en SDN.'; \
|
|
exit 2; \
|
|
fi
|
|
@if [[ -n "$(VLAN)" ]] && { ! [[ "$(VLAN)" =~ ^[0-9]+$$ ]] || (( 10#$(VLAN) < 1 || 10#$(VLAN) > 4094 )); }; then \
|
|
printf '%s\n' 'Refus: VLAN doit etre un nombre entre 1 et 4094.'; \
|
|
exit 2; \
|
|
fi
|
|
@if [[ -n "$(CLE_SSH_PUBLIQUE)" && ! -f "$(CLE_SSH_PUBLIQUE)" ]]; then \
|
|
printf '%s\n' 'Refus: cle publique SSH introuvable: $(CLE_SSH_PUBLIQUE)'; \
|
|
exit 2; \
|
|
fi
|
|
@if [[ "$(DHCP)" != "true" && ( -z "$(ADRESSE_IP)" || -z "$(CIDR)" || -z "$(PASSERELLE)" ) ]]; then \
|
|
printf '%s\n' 'Refus: fournir ADRESSE_IP, CIDR et PASSERELLE, ou utiliser DHCP=true.'; \
|
|
exit 2; \
|
|
fi
|
|
@ipconfig='ip=dhcp'; \
|
|
if [[ "$(DHCP)" != "true" ]]; then \
|
|
ipconfig='ip=$(ADRESSE_IP)/$(CIDR),gw=$(PASSERELLE)'; \
|
|
fi; \
|
|
extra_vars=( \
|
|
-e proxmox_clone_nom="$(HOTE)" \
|
|
-e proxmox_clone_vmid="$(VMID)" \
|
|
-e proxmox_clone_ipconfig0="$$ipconfig" \
|
|
-e proxmox_clone_pool="$(if $(POOL),$(POOL),$$(python3 scripts/devis_proxmox_pools.py --pool-actif))" \
|
|
); \
|
|
[[ -n "$(VMID_MODELE)" ]] && extra_vars+=( -e proxmox_clone_vmid_modele="$(VMID_MODELE)" ); \
|
|
[[ -n "$(NOEUD_PROXMOX)" ]] && extra_vars+=( -e proxmox_clone_noeud="$(NOEUD_PROXMOX)" ); \
|
|
[[ -n "$(STOCKAGE_PROXMOX)" ]] && extra_vars+=( -e proxmox_clone_stockage="$(STOCKAGE_PROXMOX)" ); \
|
|
[[ -n "$(FORMAT_DISQUE)" ]] && extra_vars+=( -e proxmox_clone_format="$(FORMAT_DISQUE)" ); \
|
|
[[ -n "$(CLONE_COMPLET)" ]] && extra_vars+=( -e proxmox_clone_complet="$(CLONE_COMPLET)" ); \
|
|
[[ -n "$(TAILLE_DISQUE)" ]] && extra_vars+=( -e proxmox_clone_taille_disque="$(TAILLE_DISQUE)" ); \
|
|
[[ -n "$(COEURS)" ]] && extra_vars+=( -e proxmox_clone_coeurs="$(COEURS)" ); \
|
|
[[ -n "$(MEMOIRE)" ]] && extra_vars+=( -e proxmox_clone_memoire="$(MEMOIRE)" ); \
|
|
[[ -n "$(MEMOIRE_MIN)" ]] && extra_vars+=( -e proxmox_clone_memoire_min="$(MEMOIRE_MIN)" ); \
|
|
[[ -n "$(DISQUE_PROXMOX)" ]] && extra_vars+=( -e proxmox_clone_disque="$(DISQUE_PROXMOX)" ); \
|
|
[[ -n "$(DNS)" ]] && extra_vars+=( -e proxmox_clone_dns="$(DNS)" ); \
|
|
[[ -n "$(DOMAINE_RECHERCHE)" ]] && extra_vars+=( -e proxmox_clone_domaines_recherche="$(DOMAINE_RECHERCHE)" ); \
|
|
[[ -n "$(CIUSER)" ]] && extra_vars+=( -e proxmox_clone_ciuser="$(CIUSER)" ); \
|
|
[[ -n "$(CLE_SSH_PUBLIQUE)" ]] && extra_vars+=( -e proxmox_clone_cle_publique_fichier="$(CLE_SSH_PUBLIQUE)" ); \
|
|
[[ -n "$${SETOPS_CLES_AMORCAGE:-}" ]] && extra_vars+=( -e "$$(python3 -c 'import json,os; print(json.dumps({"proxmox_clone_cles_amorcage": os.environ["SETOPS_CLES_AMORCAGE"]}))')" ); \
|
|
[[ -n "$(PONT_PROXMOX)" ]] && extra_vars+=( -e proxmox_clone_pont="$(PONT_PROXMOX)" ); \
|
|
extra_vars+=( -e proxmox_clone_vlan="$(VLAN)" ); \
|
|
[[ -n "$(DEMARRER)" ]] && extra_vars+=( -e proxmox_clone_demarrer="$(DEMARRER)" ); \
|
|
vault_args=(); \
|
|
vault_file=""; \
|
|
for d in lab principal production; do \
|
|
for gv in group_vars/all/vault.yml group_vars/proxmox.vault.yml; do \
|
|
f="$(SETOPS_INSTANCE)/inventories/$$d/$$gv"; \
|
|
[[ -f "$$f" ]] && vault_file="$$f" && break 2; \
|
|
done; \
|
|
done; \
|
|
if [[ -z "$$vault_file" && -L underlay.yml ]]; then \
|
|
vault_file="$$(dirname "$$(readlink -f underlay.yml)")/underlay.vault.yml"; \
|
|
fi; \
|
|
if [[ -n "$$vault_file" && -f "$$vault_file" ]]; then \
|
|
read -r premiere_ligne < "$$vault_file" || true; \
|
|
case "$$premiere_ligne" in \
|
|
'$$ANSIBLE_VAULT'*) \
|
|
if [[ -z "$${ANSIBLE_VAULT_PASSWORD_FILE:-}$${ANSIBLE_VAULT_IDENTITY_LIST:-}" ]]; then \
|
|
vault_args+=( --ask-vault-pass ); \
|
|
fi; \
|
|
;; \
|
|
esac; \
|
|
fi; \
|
|
ansible-playbook -i localhost, $(PLAYBOOK_PROXMOX_CLONER_VM) "$${vault_args[@]}" "$${extra_vars[@]}"
|
|
|
|
creer-vm: _instance-requise ## Cree une VM et attend qu'elle soit joignable — HOTE=<nom>
|
|
@set -e; \
|
|
if [[ -z "$(HOTE)" ]]; then \
|
|
printf '%s\n' 'Refus: relancer avec HOTE=nom_hote (declare dans le plan).'; \
|
|
exit 2; \
|
|
fi; \
|
|
params="$$(python3 scripts/inventory_host.py --inventaire $(INVENTAIRE_PRODUCTION) parametres-proxmox --hote $(HOTE))"; \
|
|
eval "$$params"; \
|
|
export SETOPS_CLES_AMORCAGE; \
|
|
$(MAKE) cloner-vm \
|
|
HOTE="$(HOTE)" \
|
|
VMID="$$SETOPS_VMID" \
|
|
ADRESSE_IP="$$SETOPS_IP" \
|
|
CIDR="$$SETOPS_CIDR" \
|
|
PASSERELLE="$$SETOPS_PASSERELLE" \
|
|
VLAN="$$SETOPS_VLAN" \
|
|
PONT_PROXMOX="$${SETOPS_PONT:-$(PONT_PROXMOX)}" \
|
|
STOCKAGE_PROXMOX="$${SETOPS_STOCKAGE:-$(STOCKAGE_PROXMOX)}" \
|
|
TAILLE_DISQUE="$${SETOPS_DISQUE:-$(TAILLE_DISQUE)}" \
|
|
COEURS="$${SETOPS_COEURS:-$(COEURS)}" \
|
|
MEMOIRE="$${SETOPS_MEMOIRE:-$(MEMOIRE)}" \
|
|
MEMOIRE_MIN="$${SETOPS_MEMOIRE_MIN:-$(MEMOIRE_MIN)}" \
|
|
NOEUD_PROXMOX="$${SETOPS_NOEUD:-$(NOEUD_PROXMOX)}" \
|
|
VMID_MODELE="$(VMID_MODELE)" \
|
|
FORMAT_DISQUE="$(FORMAT_DISQUE)" \
|
|
DISQUE_PROXMOX="$(DISQUE_PROXMOX)" \
|
|
DNS="$${SETOPS_DNS:-$(DNS)}" \
|
|
DOMAINE_RECHERCHE="$${SETOPS_DOMAINE:-$(DOMAINE_RECHERCHE)}" \
|
|
DHCP="$(DHCP)" \
|
|
CIUSER="$(CIUSER)" \
|
|
CLE_SSH_PUBLIQUE="$(CLE_SSH_PUBLIQUE)" \
|
|
DEMARRER="$(DEMARRER)" \
|
|
CLONE_COMPLET="$(CLONE_COMPLET)"
|
|
@# `creer-vm` CONFIRME LA MATERIALISATION, PAS LA JOIGNABILITE (2026-08-27). Il
|
|
@# attendait une reponse SSH — un geste que le runner du SITE ne peut pas faire : la
|
|
@# frontiere lui refuse d'entrer chez les tenants, et c'est le sens meme de leur
|
|
@# isolation. La premiere VM creee par le runner a donc ete declaree en echec apres
|
|
@# 600 secondes alors qu'elle tournait, avec la bonne adresse.
|
|
@#
|
|
@# L'agent invite repond a cette question sans rien ouvrir — l'API des hyperviseurs
|
|
@# est deja le flux par lequel la VM vient d'etre creee — et il PROUVE DAVANTAGE :
|
|
@# il rapporte le systeme, le noyau et l'adresse effectivement portee, la ou SSH ne
|
|
@# disait que « quelque chose ecoute sur le port 22 ».
|
|
@#
|
|
@# ATTENDRE LA DISPONIBILITE est un autre geste, deplace chez `deployer` : cloud-init
|
|
@# et dpkg se guettent juste avant de configurer, par celui qui va configurer.
|
|
@if [[ "$(ATTENDRE)" != "false" ]]; then \
|
|
params="$$(python3 scripts/inventory_host.py --inventaire $(INVENTAIRE_PRODUCTION) parametres-proxmox --hote $(HOTE))"; \
|
|
eval "$$params"; \
|
|
python3 scripts/attendre_materialisation.py \
|
|
--vmid "$$SETOPS_VMID" --ip "$$SETOPS_IP" --hote "$(HOTE)" \
|
|
--delai $(ATTENTE_HOTE); \
|
|
fi
|
|
|
|
inventaire-verifier: ansible-runtime _instance-requise ## Verifie que l'inventaire se parse (voute dechiffree)
|
|
ansible-inventory -i $(INVENTAIRE_LAB) --list > /dev/null
|
|
ansible-inventory -i $(INVENTAIRE_PRODUCTION) --list > /dev/null
|
|
python3 scripts/inventory_host.py --inventaire $(INVENTAIRE_PRODUCTION) verifier-playbooks --dossier-playbooks $(DOSSIER_PLAYBOOKS_GROUPES)
|
|
python3 scripts/inventory_host.py --inventaire $(INVENTAIRE_PRODUCTION) --dependances $(FICHIER_DEPENDANCES) verifier-dependances --dossier-playbooks $(DOSSIER_PLAYBOOKS_GROUPES)
|
|
python3 scripts/verifier_gui.py
|
|
python3 scripts/serveurs.py verifier
|
|
python3 scripts/applications.py verifier
|
|
python3 scripts/bases_donnees.py verifier
|
|
python3 scripts/domaines.py verifier
|
|
|
|
.PHONY: bases bases-verifier domaines domaines-verifier applications applications-verifier applications-bootstrap serveurs serveurs-verifier serveurs-bootstrap
|
|
serveurs: ## Liste les serveurs declares au plan
|
|
python3 scripts/serveurs.py lister
|
|
|
|
serveurs-verifier: ## Valide le registre des serveurs
|
|
python3 scripts/serveurs.py verifier
|
|
|
|
serveurs-bootstrap: ## REPRISE : (re)constitue plan/serveurs.yml depuis un inventaire existant
|
|
python3 scripts/serveurs.py bootstrap
|
|
|
|
.PHONY: instancier instancier-appliquer
|
|
instancier: _instance-requise ## Genere hosts.yml depuis le plan (sans l'appliquer)
|
|
python3 scripts/instancier.py generer
|
|
python3 scripts/instancier.py comparer
|
|
|
|
instancier-appliquer: _instance-requise ## Applique l'inventaire genere — FORCE=1 pour passer outre le diff
|
|
python3 scripts/instancier.py appliquer $(if $(FORCE),--force)
|
|
|
|
bases: ## Liste les bases de donnees declarees au plan
|
|
python3 scripts/bases_donnees.py lister
|
|
|
|
bases-verifier: ## Valide le registre des bases de donnees
|
|
python3 scripts/bases_donnees.py verifier
|
|
|
|
domaines: ## Liste les domaines declares au plan
|
|
python3 scripts/domaines.py lister
|
|
|
|
domaines-verifier: ## Valide le registre des domaines
|
|
python3 scripts/domaines.py verifier
|
|
|
|
applications: ## Liste les applications declarees au plan
|
|
python3 scripts/applications.py lister
|
|
|
|
applications-verifier: ## Valide le registre des applications
|
|
python3 scripts/applications.py verifier
|
|
|
|
applications-bootstrap: ## Amorce les applications declarees au plan
|
|
python3 scripts/applications.py bootstrap
|
|
|
|
inventaire-lister: ansible-runtime ## Affiche l'inventaire complet (JSON)
|
|
ansible-inventory -i $(FICHIER_INVENTAIRE) --list
|
|
|
|
inventaire-graphe: ansible-runtime ## Affiche le graphe des groupes de l'inventaire
|
|
ansible-inventory -i $(FICHIER_INVENTAIRE) --graph
|
|
|
|
inventaire-hote: ansible-runtime ## Affiche les variables derivees d'un hote — HOTE=<nom>
|
|
@if [[ -z "$(HOTE)" ]]; then \
|
|
printf '%s\n' 'Refus: relancer avec HOTE=nom_hote.'; \
|
|
exit 2; \
|
|
fi
|
|
ansible-inventory -i $(FICHIER_INVENTAIRE) --host $(HOTE)
|
|
|
|
inventaire-lab: ## Affiche le graphe de l'inventaire de laboratoire
|
|
$(MAKE) inventaire-graphe FICHIER_INVENTAIRE="$(INVENTAIRE_LAB)"
|
|
|
|
inventaire-production: ## Affiche le graphe de l'inventaire de production
|
|
$(MAKE) inventaire-graphe FICHIER_INVENTAIRE="$(INVENTAIRE_PRODUCTION)"
|
|
|
|
.PHONY: _verifier-acces-modele _verifier-privileges-modele preparer-modele verifier-modele nettoyer-modele
|
|
_verifier-acces-modele: ansible-runtime _modele-requis
|
|
ansible -i $(INVENTAIRE_MODELE) $(CIBLE_MODELE) $(UTILISATEUR_MODELE) -m ping -e ansible_become=false
|
|
|
|
_verifier-privileges-modele: ansible-runtime _modele-requis
|
|
ansible -i $(INVENTAIRE_MODELE) $(CIBLE_MODELE) $(UTILISATEUR_MODELE) -b -m command -a "whoami"
|
|
|
|
# Le gabarit dore n'est PAS un hote du tenant : sa configuration Proxmox le place sur le
|
|
# reseau de fabrication (192.168.12.x), pas dans le supernet. `instancier` emet donc
|
|
# `modeles_vm` toujours VIDE, et les etats d'un serveur ne connaissent que `actif` et
|
|
# `planifie` — rien ne pouvait y entrer. Resultat mesure le 2026-08-09 : trois cibles
|
|
# documentees qui ne pouvaient rien faire, et Ansible qui repondait « skipping: no hosts
|
|
# matched » sans que ce soit une erreur.
|
|
#
|
|
# On DESIGNE donc la machine explicitement : `MODELE_HOTE=<ip-ou-nom>`. A defaut, on
|
|
# refuse au lieu de ne rien faire — une commande qui ne fait rien en silence est pire
|
|
# qu'une commande absente.
|
|
, := ,
|
|
|
|
_modele-requis:
|
|
@if [[ -z "$(MODELE_HOTE)" ]] && ! ansible-inventory -i $(INVENTAIRE_LAB) --list 2>/dev/null | python3 -c 'import json,sys; d=json.load(sys.stdin); sys.exit(0 if (d.get("modeles_vm") or {}).get("hosts") else 1)' 2>/dev/null; then \
|
|
printf '%s\n' 'Refus: aucune VM de gabarit designee.'; \
|
|
printf '%s\n' ' Le gabarit vit sur le reseau de fabrication, pas dans le tenant :'; \
|
|
printf '%s\n' ' il ne peut pas venir du plan. Le designer explicitement —'; \
|
|
printf '%s\n' ' make preparer-modele MODELE_HOTE=192.168.12.99'; \
|
|
printf '%s\n' ' make verifier-modele MODELE_HOTE=192.168.12.99'; \
|
|
printf '%s\n' ' make nettoyer-modele MODELE_HOTE=192.168.12.99 CONFIRMER=true'; \
|
|
printf '%s\n' ' (la VM doit etre DEMARREE : un template Proxmox ne boote pas ;'; \
|
|
printf '%s\n' ' le convertir en VM ou en cloner une copie de travail.)'; \
|
|
exit 2; \
|
|
fi
|
|
|
|
# `-i "<hote>,"` : la virgule finale fait de la chaine un inventaire d'un seul hote.
|
|
INVENTAIRE_MODELE = $(if $(MODELE_HOTE),"$(MODELE_HOTE)$(,)",$(INVENTAIRE_LAB))
|
|
# Cible : la machine designee, ou le groupe si l'inventaire en contient un.
|
|
CIBLE_MODELE = $(if $(MODELE_HOTE),$(MODELE_HOTE),$(GROUPE_MODELE))
|
|
|
|
# Un inventaire d'UN SEUL HOTE (`-i "<ip>,"`) ne porte aucun `group_vars` : il faut donc
|
|
# lui donner l'utilisateur de connexion, sinon Ansible tente le compte local de
|
|
# l'operateur. Defaut `ansible`, qui est le `ciuser` du gabarit.
|
|
MODELE_UTILISATEUR ?= ansible
|
|
UTILISATEUR_MODELE = $(if $(MODELE_HOTE),-e ansible_user=$(MODELE_UTILISATEUR) -e cible_modele=$(MODELE_HOTE),)
|
|
|
|
preparer-modele: ansible-runtime _modele-requis _verifier-acces-modele _verifier-privileges-modele ## Prepare le gabarit dore (VM de reference clonee pour chaque hote)
|
|
ansible-playbook -i $(INVENTAIRE_MODELE) $(UTILISATEUR_MODELE) $(PLAYBOOK_PREPARER_MODELE)
|
|
|
|
verifier-modele: ansible-runtime _modele-requis ## Verifie le gabarit dore
|
|
ansible-playbook -i $(INVENTAIRE_MODELE) $(UTILISATEUR_MODELE) $(PLAYBOOK_VERIFIER_MODELE)
|
|
|
|
nettoyer-modele: ansible-runtime _modele-requis ## Nettoie le gabarit avant capture — exige CONFIRMER=true
|
|
@if [[ "$(CONFIRMER)" != "true" ]]; then \
|
|
printf '%s\n' 'Refus: relancer avec CONFIRMER=true pour le nettoyage final du modele.'; \
|
|
exit 2; \
|
|
fi
|
|
ansible-playbook -i $(INVENTAIRE_MODELE) $(UTILISATEUR_MODELE) $(PLAYBOOK_NETTOYER_MODELE) -e template_cleanup_confirm=true
|
|
|
|
.PHONY: _verifier-acces-hote _verifier-privileges-hote _attendre-hote faits verifier-hote
|
|
|
|
# Deux ATTENTES ACTIVES, sans lesquelles on ne peut pas enchainer creation et
|
|
# deploiement — donc sans lesquelles `make myDay` ne peut pas reconstruire seul.
|
|
#
|
|
# 1. SSH. `make creer-vm` rend la main des que Proxmox a DEMARRE la VM, pas quand elle
|
|
# repond. Verifier l'acces aussitot echoue presque toujours sur une machine neuve —
|
|
# et le symptome trompe : a travers la frontiere, le TCP s'etablit (SYN proxy) et
|
|
# l'echec se lit « Connection timed out during banner exchange ».
|
|
#
|
|
# 2. Le verrou dpkg. L'image Debian lance ses propres mises a jour au premier
|
|
# demarrage et tient `/var/lib/dpkg/lock-frontend` plusieurs minutes. Le premier
|
|
# `apt` d'Ansible echoue alors sur un verrou, pas sur une vraie erreur.
|
|
#
|
|
# `unattended-upgrades.service` est volontairement ABSENT de la condition : c'est un
|
|
# DEMON (Type=simple), toujours `active`. L'y inclure rendait l'attente impossible a
|
|
# satisfaire — elle echouait au bout du delai, systematiquement. Seules `apt-daily*`
|
|
# sont des one-shot, et c'est le VERROU qui dit si dpkg est reellement occupe.
|
|
#
|
|
# Les deux sont des COURSES de premier demarrage, pas des defauts de conception. On les
|
|
# attend au lieu de les subir. ATTENTE_HOTE (secondes) borne chacune : depasser le
|
|
# delai reste un echec, pour ne pas transformer une panne en attente infinie.
|
|
ATTENTE_HOTE ?= 600
|
|
|
|
_attendre-hote: ansible-runtime
|
|
@set -e; \
|
|
if [[ -z "$(LIMITE)" ]]; then printf '%s\n' 'Refus: LIMITE requis.'; exit 2; fi; \
|
|
fin=$$(( SECONDS + $(ATTENTE_HOTE) )); \
|
|
printf '%s' "Attente de SSH sur $(LIMITE) "; \
|
|
until ansible -i $(INVENTAIRE_PRODUCTION) $(LIMITE) -m ping -e ansible_become=false >/dev/null 2>&1; do \
|
|
if (( SECONDS > fin )); then printf '%s\n' " ECHEC: injoignable apres $(ATTENTE_HOTE)s."; exit 4; fi; \
|
|
printf '.'; sleep 5; \
|
|
done; \
|
|
printf '%s\n' " ok"; \
|
|
printf '%s' "Attente de cloud-init et des maj automatiques "; \
|
|
until ansible -i $(INVENTAIRE_PRODUCTION) $(LIMITE) -b -m shell -a \
|
|
'cloud-init status --wait >/dev/null 2>&1; \
|
|
! systemctl is-active --quiet apt-daily.service apt-daily-upgrade.service \
|
|
&& ! fuser /var/lib/dpkg/lock-frontend >/dev/null 2>&1' >/dev/null 2>&1; do \
|
|
if (( SECONDS > fin )); then printf '%s\n' " ECHEC: dpkg toujours occupe apres $(ATTENTE_HOTE)s."; exit 4; fi; \
|
|
printf '.'; sleep 10; \
|
|
done; \
|
|
printf '%s\n' " libere"
|
|
|
|
_verifier-acces-hote: ansible-runtime
|
|
ansible -i $(INVENTAIRE_PRODUCTION) $(LIMITE) -m ping -e ansible_become=false
|
|
|
|
_verifier-privileges-hote: ansible-runtime
|
|
ansible -i $(INVENTAIRE_PRODUCTION) $(LIMITE) -b -m command -a "whoami"
|
|
|
|
faits: ansible-runtime ## Interroge les faits Ansible de la flotte — LIMITE=<motif>
|
|
ansible -i $(INVENTAIRE_PRODUCTION) $(LIMITE) -m setup -a "filter=ansible_distribution*"
|
|
|
|
verifier-hote: ansible-runtime ## Passe le playbook de verification sur un hote
|
|
ansible-playbook -i $(INVENTAIRE_PRODUCTION) $(PLAYBOOK_VERIFIER_HOTE) $(OPTIONS_PLAYBOOK)
|