Set-OPS-Public/Makefile

878 lines
39 KiB
Makefile
Raw Normal View History

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
# 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
SETOPS_INVENTAIRE ?= $(firstword $(wildcard $(SETOPS_INSTANCE)/inventories/principal/hosts.yml) $(wildcard $(SETOPS_INSTANCE)/inventories/production/hosts.yml) $(SETOPS_INSTANCE)/inventories/principal/hosts.yml)
export SETOPS_INVENTAIRE
# Inventaire « modèle » (construction du golden template) : lab > principal > production.
INVENTAIRE_LAB ?= $(firstword $(wildcard $(SETOPS_INSTANCE)/inventories/lab/hosts.yml) $(wildcard $(SETOPS_INSTANCE)/inventories/principal/hosts.yml) $(SETOPS_INSTANCE)/inventories/production/hosts.yml)
INVENTAIRE_PRODUCTION ?= $(SETOPS_INVENTAIRE)
FICHIER_INVENTAIRE ?= $(SETOPS_INVENTAIRE)
FICHIER_DEPENDANCES ?= docs/dependances-groupes.yml
GROUPE_MODELE ?= modeles_vm
GROUPE_DEBIAN ?= serveur_debian
GROUPE_HOTES_ACTIFS ?= hotes_actifs
LIMITE ?= $(GROUPE_DEBIAN)
HOTE ?=
ADRESSE_IP ?=
GROUPES ?= $(GROUPE_DEBIAN)
GROUPE ?= $(GROUPE_DEBIAN)
UTILISATEUR_ANSIBLE ?= ansible
VMID_MODELE ?=
VMID ?=
NOEUD_PROXMOX ?=
STOCKAGE_PROXMOX ?=
FORMAT_DISQUE ?=
TAILLE_DISQUE ?=
DISQUE_PROXMOX ?=
CIDR ?= 24
PASSERELLE ?=
DNS ?=
DHCP ?= false
CIUSER ?=
CLE_SSH_PUBLIQUE ?=
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:
@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:
@printf '%s\n' 'Set-OPS — moteur d ecosystemes numeriques souverains'
@printf '%s\n' ''
@printf '%s\n' 'Nouveau ? -> QUICKSTART.md (de zero a ton ecosysteme sur Proxmox)'
@printf '%s\n' 'Flux: editer le plan -> make instancier -> make instancier-appliquer -> make deployer'
@printf '%s\n' ''
@printf '%s\n' 'VM'
@printf '%s\n' ' Creer une VM (VMID/IP/VLAN/passerelle lus dans le plan):'
@printf '%s\n' ' make creer-vm HOTE=web-frontal-01'
@printf '%s\n' ' Cloner seulement, sans passer par le plan:'
@printf '%s\n' ' make cloner-vm HOTE=web-frontal-01 VMID=95301 VLAN=15 ADRESSE_IP=10.0.2.31 PASSERELLE=10.0.2.1'
@printf '%s\n' ' Configurer Proxmox et le Vault API (parametres: docs/config-proxmox.md):'
@printf '%s\n' ' make config'
@printf '%s\n' ''
@printf '%s\n' 'Hotes'
@printf '%s\n' ' Planifier/modifier un hote: editer le plan, puis regenerer:'
@printf '%s\n' ' editer instance/plan/serveurs.yml (ou la vue Serveurs du GUI)'
@printf '%s\n' ' make instancier-appliquer'
@printf '%s\n' ' Verifier un deploiement a blanc (dry-run):'
@printf '%s\n' ' make verifier-deploiement HOTE=web-frontal-01'
@printf '%s\n' ' Remettre un hote en conformite selon ses groupes:'
@printf '%s\n' ' make deployer HOTE=web-frontal-01'
@printf '%s\n' ' Afficher un hote:'
@printf '%s\n' ' make hote-afficher HOTE=web-frontal-01'
@printf '%s\n' ' Diagnostiquer:'
@printf '%s\n' ' make verifier-hote LIMITE=web-frontal-01'
@printf '%s\n' ''
@printf '%s\n' 'Groupes'
@printf '%s\n' ' Appliquer un groupe complet:'
@printf '%s\n' ' make deployer-groupe GROUPE=serveur_debian'
@printf '%s\n' ' Convention:'
@printf '%s\n' ' groupe serveur_debian -> playbooks/groupes/serveur_debian.yml'
@printf '%s\n' ''
@printf '%s\n' 'Ecosysteme complet (orchestrateur)'
@printf '%s\n' ' Ordre de deploiement (couches + graphe):'
@printf '%s\n' ' make site-verifier # valide la coherence couches/graphe'
@printf '%s\n' ' python3 scripts/orchestrer.py ordre'
@printf '%s\n' ' (Re)generer playbooks/site.yml ordonne:'
@printf '%s\n' ' make site'
@printf '%s\n' ' CONFIGURER la flotte existante, couche par couche (2b, VM deja creees):'
@printf '%s\n' ' make deployer-tout CONFIRMER=true # (MODE_CHECK=1 pour un essai a blanc idempotent)'
@printf '%s\n' ' CREER toutes les VM du plan (2a, clone Proxmox):'
@printf '%s\n' ' make flotte-creer CONFIRMER=true'
@printf '%s\n' ' RECONSTRUIRE from-zero = creer les VM PUIS deployer (2a+2b, VM inexistantes):'
@printf '%s\n' ' make reconstruire CONFIRMER=true # alias: make myDay CONFIRMER=true'
@printf '%s\n' ''
@printf '%s\n' 'Flux reseau (pare-feu / audit)'
@printf '%s\n' ' Matrice d audit + apercus nftables resolus (NON actives):'
@printf '%s\n' ' make flux # -> docs/registre-flux.md + instance/flux-genere/*.nft'
@printf '%s\n' ' make flux-verifier # valide schema + coherence de matrice'
@printf '%s\n' ''
@printf '%s\n' 'Wiki pedagogique'
@printf '%s\n' ' Publier wiki/ dans le wiki Forgejo (source versionnee -> vue browsable):'
@printf '%s\n' ' make wiki-publier WIKI_REMOTE=https://forge.<domaine>/<proprio>/<depot>.wiki.git'
@printf '%s\n' ''
@printf '%s\n' 'Inventaires'
@printf '%s\n' ' Graphe de production:'
@printf '%s\n' ' make inventaire'
@printf '%s\n' ' Graphe explicite:'
@printf '%s\n' ' make inventaire-graphe FICHIER_INVENTAIRE=$(SETOPS_INSTANCE)/inventories/production/hosts.yml'
@printf '%s\n' ' Verifier les inventaires:'
@printf '%s\n' ' make inventaire-verifier'
@printf '%s\n' ' Lister les donnees brutes:'
@printf '%s\n' ' make inventaire-lister'
@printf '%s\n' ' Interface locale de gestion:'
@printf '%s\n' ' make inventaire-ui'
@printf '%s\n' ''
@printf '%s\n' 'Modele Debian 13 Proxmox'
@printf '%s\n' ' Construire et verifier:'
@printf '%s\n' ' make preparer-modele'
@printf '%s\n' ' make verifier-modele'
@printf '%s\n' ' Nettoyage final protege:'
@printf '%s\n' ' make nettoyer-modele CONFIRMER=true'
@printf '%s\n' ''
@printf '%s\n' 'Validation'
@printf '%s\n' ' make syntaxe'
@printf '%s\n' ' make lint'
@printf '%s\n' ' make verifier'
@printf '%s\n' ''
@printf '%s\n' 'Variables frequentes'
@printf '%s\n' ' HOTE=web-frontal-01 GROUPE=serveur_debian GROUPES="serveur_debian serveur_durci"'
@printf '%s\n' ' VMID=95301 VLAN=15 ADRESSE_IP=10.0.2.31 PASSERELLE=10.0.2.1'
@printf '%s\n' ' FICHIER_INVENTAIRE=$(SETOPS_INSTANCE)/inventories/production/hosts.yml FICHIER_DEPENDANCES=docs/dependances-groupes.yml CONFIRMER=true'
.PHONY: lint
lint: ansible-runtime
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
syntaxe-modele: ansible-runtime
ansible-playbook -i $(INVENTAIRE_LAB) $(PLAYBOOK_PREPARER_MODELE) --syntax-check
syntaxe-verification-modele: ansible-runtime
ansible-playbook -i $(INVENTAIRE_LAB) $(PLAYBOOK_VERIFIER_MODELE) --syntax-check
syntaxe-nettoyage: ansible-runtime
ansible-playbook -i $(INVENTAIRE_LAB) $(PLAYBOOK_NETTOYER_MODELE) --syntax-check
syntaxe-verification-hote: ansible-runtime
ansible-playbook -i $(INVENTAIRE_PRODUCTION) $(PLAYBOOK_VERIFIER_HOTE) --syntax-check
syntaxe-groupes: ansible-runtime
@for playbook in $(DOSSIER_PLAYBOOKS_GROUPES)/*.yml; do \
ansible-playbook -i $(INVENTAIRE_PRODUCTION) "$$playbook" --syntax-check; \
done
syntaxe-proxmox: ansible-runtime
ansible-playbook -i localhost, $(PLAYBOOK_PROXMOX_CLONER_VM) --syntax-check
.PHONY: test
test:
python3 scripts/tests/test_inventory_host.py
.PHONY: verifier
verifier: lint test inventaire-verifier site-verifier flux-verifier syntaxe
python3 scripts/prouver.py --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
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
# 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:
@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:
@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:
@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:
@python3 scripts/plan_recette.py
# Liste les modèles disponibles (socle + SETOPS_MODELES) pour créer une instance.
instance-modeles:
@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:
@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:
@python3 scripts/model_creer.py --mode "$(MODE)" --nom "$(NOM)" \
$(if $(BASE),--base $(BASE),) $(if $(SOURCE),--source $(SOURCE),) $(if $(DEST),--dest $(DEST),)
config:
python3 scripts/config_proxmox.py
inventaire-ui: _instance-requise
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
@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
@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)'
deployer: _instance-requise
@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:-}" ]]; 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="$(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
python3 scripts/orchestrer.py ecrire
ansible-playbook -i $(INVENTAIRE_PRODUCTION) playbooks/site.yml --syntax-check
site-verifier:
python3 scripts/orchestrer.py verifier
.PHONY: flux flux-verifier
flux: ansible-runtime
python3 scripts/resoudre_flux.py registre
python3 scripts/resoudre_flux.py nftables
.PHONY: devis-reseau
devis-reseau: ansible-runtime ## Devis switch (VLANs/SVIs/ACLs) du reseau converge, derive des nomenclatures. DIALECTE=cisco|binardat
python3 scripts/devis_reseau.py $(if $(DIALECTE),--dialecte $(DIALECTE),)
frontière nord/sud : devis dérivé, lien de transit et les deux routes La bordure devient un artefact dérivé, comme le devis switch — et le chemin qui y mène est enfin déclaré. `make devis-opnsense` (+ preuve P24) dérive la politique de bordure du registre des flux : les flux `pair: externe`, que `resoudre_flux.py` saute volontairement parce qu'ils relèvent de la frontière et non du pare-feu d'hôte. Aucun port, aucune adresse, aucun nom d'hôte dans le générateur. Le lien manquait dans tous les fichiers : le devis switch ne contenait pas une seule `ip route`. Un réseau underlay portant `passerelle_sortie` le déclare — il vit dans l'underlay et non dans un tenant parce que la frontière route vers TOUS les supernets tenants par le même saut, donc il ne peut dériver d'aucun `index`. `devis-reseau` en tire deux routes : l'aller (sortie générale) et le retour vers l'administration, dont l'absence a coûté la passe de déploiement du 2026-07-29 — la réponse revient au pare-feu par une autre interface que celle où l'état a été créé, et se fait jeter en silence. Les réseaux d'administration viennent de l'intrant `nftables_admin_ssh` : même source unique que la garde anti-lockout des nftables et l'alias SETOPS_ADMIN. Les trois pare-feux et les routes ne peuvent plus diverger. La frontière est réglable depuis la console (section « Frontière » du panneau Intrants) ; les identifiants d'API restent interdits d'écriture par le GUI et vivent dans la voûte. Correctifs de la même passe : - le panneau refusait d'enregistrer les intrants de la frontière : le garde-fou confondait une référence de voûte `{{ vault_* }}` préservée avec un secret soumis. Il regarde désormais la valeur, pas le nom. - `supprimer_vm_debian.yml` ne chargeait que `proxmox.vault.yml` pour ses secrets ; retirer ce reliquat aurait cassé `make detruire`. Aligné sur le playbook de clonage, voûte unique en dernier. - documentation : la voûte est unique, `proxmox.vault.yml` n'est qu'un reliquat de compatibilité. Preuves : 24 OK, 0 échec. Cas de rejet du validateur d'underlay exercés un par un ; résolution du jeton Proxmox vérifiée en exécution réelle. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:32:04 -04:00
.PHONY: devis-opnsense devis-opnsense-verifier
devis-opnsense: ansible-runtime ## Devis OPNsense (frontiere nord/sud), derive du registre des flux (pair: externe)
python3 scripts/devis_opnsense.py $(if $(JSON),--json,)
.PHONY: proxmox-fw-plan proxmox-fw-appliquer
proxmox-fw-plan: ansible-runtime ## Ecart entre le pare-feu est-ouest Proxmox et son devis (aucune ecriture)
python3 scripts/appliquer_proxmox_fw.py
proxmox-fw-appliquer: ansible-runtime ## Reconcilie le pare-feu est-ouest : IPSets, groupes, affectations. CONFIRMER=true
@if [[ "$(CONFIRMER)" != "true" ]]; then \
printf '%s\n' "Refus: cette cible ECRIT sur le pare-feu du cluster (IPSets, groupes, VM)" \
"et RETIRE ce qui est perime. Elle n'active JAMAIS le pare-feu du datacenter." \
"Relire d'abord 'make proxmox-fw-plan', puis: make proxmox-fw-appliquer CONFIRMER=true"; \
exit 2; fi
CONFIRMER=true python3 scripts/appliquer_proxmox_fw.py
.PHONY: sdn-plan sdn-appliquer
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
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
devis des certificats : disque contre memoire, et l'AC etait expiree Deuxieme application du patron devis/applicateur aux services. Trouve a la premiere execution : sur infra-pki-01 — l'autorite elle-meme — le certificat etait expire depuis plus de 8 h et le renouvellement echouait toutes les 14 minutes sur « 'step ca renew' requires the '--ca-url' flag ». Rien ne le signalait. Cause : sur l'hote de l'AC, /etc/step est le STEPPATH du SERVEUR, pas un amorcage client — pas de defaults.json, et l'unite de renouvellement en dependait. La lecon etait deja ecrite dans le commentaire de la tache d'emission (« l'autorite ne bootstrape pas »), jamais reportee sur l'unite. Le role ne pouvait pas non plus se soigner : la re-emission ne regardait que la FORME (cert absent ou SAN manquant), jamais la validite. client_pki verifie desormais l'echeance (client_pki_marge_renouvellement). Ce qu'il a fallu desapprendre : les certificats vivent 24 h et se renouvellent toutes les ~14 min ; « empreinte servie != empreinte disque » est l'etat NORMAL. Comparer les empreintes aurait donne un verificateur qui crie en permanence. Le signal est l'echeance de ce qui est SERVI, plus l'absence de client_pki_reload_services. Le devis a d'abord menti, du defaut meme qu'il traque : include_vars au niveau du play prime sur les group_vars. Et le premier correctif a PARU marcher — set_fact accepte un dictionnaire entier en argument libre sans erreur et n'en fait rien. Il faut reimposer cle par cle. Les deux devis sont corriges et le piege est consigne dans docs/devis-services.md avant d'ecrire le prochain. Verifie dans les deux sens : CONFORME sur 14 hotes ; sur un releve ou l'on rejoue une copie perimee en memoire, 2 ecarts et code de sortie 1. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 07:20:56 -04:00
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
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:
python3 scripts/devis_proxmox_fw.py --verifier
pools Proxmox : un par tenant, dérivé de l'index (D-37, P28) Onze des quatorze serveurs portent le même nom court chez Chezlepro et chez Technolibre. Vérifié un par un, ce n'est pas un problème technique : tout le reste dérive du seed et diverge (10.27.19.21 contre 10.21.19.21, VMID 117402101 contre 111402101, VLAN 1174 contre 1114, deux domaines internes), et rien n'est indexé sur le nom court — les opérations Proxmox portent toutes un vmid, les certificats un FQDN, et client_backup_repo vise backup-01.{{ domaine_interne }}. Le coût est humain : la console Proxmox affiche le nom, et deux infra-pki-01 y sont indiscernables à l'œil. Le VMID porte le tenant, encore faut-il connaître le codage. Un pool par tenant, dérivé du dossier d'instance et de l'index — déjà unique par P21, donc aucun registre de plus : Chezlepro-17, Technolibre-11. make devis-proxmox-pools rattrape la flotte existante (création du pool, puis affectation des VM actives). Les VM créées ensuite entrent d'elles-mêmes : make creer-vm dérive le pool par la même fonction et le passe à la création. Le playbook crée le pool au préalable — proxmox_kvm échoue sur un pool inconnu, et l'API ne sait pas changer le pool d'une VM existante ; c'est aussi pourquoi le rattrapage passe par les membres. P28 garde deux collisions : même nom de pool entre tenants, et surtout même VMID — une machine appartenant à deux tenants serait pire qu'une homonymie. Rien n'est renommé : les homonymes sont la preuve que la nomenclature est un vrai gabarit. Le devis ne lit pas le cluster, il dit l'état cible et non l'écart. 27 preuves OK, 0 échec. --syntax-check du playbook de clonage. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 15:15:14 -04:00
.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:
python3 scripts/devis_proxmox_pools.py --verifier
SDN : make devis-sdn — zone, VNets et sous-réseaux dérivés du seed (D-43/44, P30) Ajouter un tenant implique 1 zone EVPN + 6 VNets + 6 sous-réseaux sur le cluster. Aucun générateur ne les produisait : dernière lacune dans un dépôt où tout dérive. 26 objets pour les deux tenants, VNI = 1000 + index×10 + zone, sous-réseau et passerelle par les mêmes fonctions que l'inventaire. Nommage, en deux temps. D'abord VRF0017 / v1174, alignés sur ce que le cluster portait — réflexe inverse du bon : cette convention venait d'une création à la main et ne disait pas de quel tenant il s'agissait. Forme retenue : t<index> pour la zone, t<index><zone abrégée> pour le VNet (t17, t17serv). C'est le préfixe que le pare-feu Proxmox utilisait déjà (t17-cli-metrique), donc un seul schéma dans tout le dépôt. Abréviation = 4 premières lettres du libellé, accents retirés. Pas de tiret entre index et zone, contrairement aux IPSets : t245-serv ferait 9 caractères, t245serv en fait 8 — la forme reste uniforme jusqu'au dernier index. Contrainte cadrante : zones ET VNets sont limités à 8 caractères par Proxmox. sdn-evpn.md annonçait chez17-services-infra (21) : il aurait été refusé à l'application. P30 refuse tout dépassement et toute collision de nom, de VNI ou de sous-réseau. Éprouvé aux bornes et par sabotage. Vérification la plus forte : avant renommage, la dérivation reproduisait à l'identique les deux zones créées à la main — nom, VNI de VRF, MTU, contrôleur. voute.py saisir : le pendant de la génération. On génère un secret dont le dépôt est la source, on saisit celui dont un tiers est la source — inventer une clé d'API OPNsense donnerait une valeur refusée à la première requête, avec P18 au vert sur une voûte inutilisable. Sans écho, double confirmation, rien sur la ligne de commande. Reste ouvert : aucun nœud de sortie déclaré. Le devis émet un marqueur, pas une valeur plausible. Deux points à trancher — le nœud de sortie route selon sa propre table (défaut actuel : 192.168.11.254, pas la frontière), et l'entrée n'est pas redondante puisqu'elle dépend d'une route statique d'OPNsense vers un seul nœud. D-45 : l'affinité de VM attend Proxmox 9 (cluster en 8.4.19), tenue à la main. 30 preuves OK. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 11:06:42 -04:00
.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:
python3 scripts/devis_sdn.py --verifier
frontière nord/sud : devis dérivé, lien de transit et les deux routes La bordure devient un artefact dérivé, comme le devis switch — et le chemin qui y mène est enfin déclaré. `make devis-opnsense` (+ preuve P24) dérive la politique de bordure du registre des flux : les flux `pair: externe`, que `resoudre_flux.py` saute volontairement parce qu'ils relèvent de la frontière et non du pare-feu d'hôte. Aucun port, aucune adresse, aucun nom d'hôte dans le générateur. Le lien manquait dans tous les fichiers : le devis switch ne contenait pas une seule `ip route`. Un réseau underlay portant `passerelle_sortie` le déclare — il vit dans l'underlay et non dans un tenant parce que la frontière route vers TOUS les supernets tenants par le même saut, donc il ne peut dériver d'aucun `index`. `devis-reseau` en tire deux routes : l'aller (sortie générale) et le retour vers l'administration, dont l'absence a coûté la passe de déploiement du 2026-07-29 — la réponse revient au pare-feu par une autre interface que celle où l'état a été créé, et se fait jeter en silence. Les réseaux d'administration viennent de l'intrant `nftables_admin_ssh` : même source unique que la garde anti-lockout des nftables et l'alias SETOPS_ADMIN. Les trois pare-feux et les routes ne peuvent plus diverger. La frontière est réglable depuis la console (section « Frontière » du panneau Intrants) ; les identifiants d'API restent interdits d'écriture par le GUI et vivent dans la voûte. Correctifs de la même passe : - le panneau refusait d'enregistrer les intrants de la frontière : le garde-fou confondait une référence de voûte `{{ vault_* }}` préservée avec un secret soumis. Il regarde désormais la valeur, pas le nom. - `supprimer_vm_debian.yml` ne chargeait que `proxmox.vault.yml` pour ses secrets ; retirer ce reliquat aurait cassé `make detruire`. Aligné sur le playbook de clonage, voûte unique en dernier. - documentation : la voûte est unique, `proxmox.vault.yml` n'est qu'un reliquat de compatibilité. Preuves : 24 OK, 0 échec. Cas de rejet du validateur d'underlay exercés un par un ; résolution du jeton Proxmox vérifiée en exécution réelle. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:32:04 -04:00
devis-opnsense-verifier:
python3 scripts/devis_opnsense.py --verifier
.PHONY: underlay
underlay: ## Underlay (fabric physique cluster-global : mgmt/iSCSI/Ceph) : affiche + valide (P23)
python3 scripts/underlay.py
flux-verifier:
python3 scripts/resoudre_flux.py verifier
.PHONY: valider
valider: ansible-runtime
ansible-playbook -i $(INVENTAIRE_PRODUCTION) playbooks/valider.yml
.PHONY: wiki-publier
wiki-publier:
@set -e; \
if [[ -z "$(WIKI_REMOTE)" ]]; then \
printf '%s\n' 'Refus: URL du wiki Forgejo requise.'; \
printf '%s\n' 'Ex: make wiki-publier WIKI_REMOTE=https://forge.<domaine>/<proprio>/<depot>.wiki.git'; \
exit 2; \
fi; \
src="$(CURDIR)/wiki"; \
tmp="$$(mktemp -d)"; \
trap 'rm -rf "$$tmp"' EXIT; \
printf '%s\n' "Clonage du wiki: $(WIKI_REMOTE)"; \
if ! git clone --quiet --depth 1 "$(WIKI_REMOTE)" "$$tmp/wiki"; then \
printf '%s\n' 'Echec du clone (URL ou acces ?). Le wiki doit exister (creer une 1re page dans Forgejo).'; \
exit 1; \
fi; \
find "$$tmp/wiki" -maxdepth 1 -name '*.md' -delete; \
for f in "$$src"/*.md; do \
bn="$$(basename "$$f")"; \
[[ "$$bn" == "README.md" ]] && continue; \
cp "$$f" "$$tmp/wiki/$$bn"; \
done; \
rm -rf "$$tmp/wiki/img"; \
if compgen -G "$$src/img/*-annote.svg" > /dev/null; then \
mkdir -p "$$tmp/wiki/img"; \
cp -L "$$src"/img/*-annote.svg "$$tmp/wiki/img/"; \
fi; \
cd "$$tmp/wiki"; \
if [[ -z "$$(git status --porcelain)" ]]; then \
printf '%s\n' 'Wiki deja a jour (aucun changement).'; \
exit 0; \
fi; \
git add -A; \
sha="$$(git -C "$(CURDIR)" rev-parse --short HEAD 2>/dev/null || echo inconnu)"; \
git commit --quiet -m "Publication du wiki depuis le depot (source: $$sha)"; \
git push --quiet; \
printf '%s\n' 'Wiki publie.'
deployer-tout: _instance-requise
@set -e; \
if [[ "$(CONFIRMER)" != "true" ]]; then \
printf '%s\n' 'Refus: deploiement ORCHESTRE de TOUTE la flotte (action impactante).'; \
printf '%s\n' 'Relancer avec CONFIRMER=true. Astuce: tester d abord en idempotent avec MODE_CHECK=1.'; \
exit 2; \
fi; \
python3 scripts/orchestrer.py verifier; \
python3 scripts/orchestrer.py ecrire; \
vault_chiffre="$$(grep -rlsIF '$$ANSIBLE_VAULT' $(dir $(INVENTAIRE_PRODUCTION))group_vars 2>/dev/null | head -1 || true)"; \
if [[ -n "$$vault_chiffre" && -z "$${ANSIBLE_VAULT_PASSWORD_FILE:-}" ]]; 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
flotte-creer: _instance-requise
@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; \
for h in $$hotes; do \
printf '\n=== Creation VM: %s ===\n' "$$h"; \
$(MAKE) creer-vm HOTE="$$h"; \
done; \
printf '\nToutes les VM actives 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
reconstruire: _instance-requise
@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) flotte-creer CONFIRMER=true; \
$(MAKE) _attendre-flotte; \
$(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
myDay: reconstruire
deployer-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
@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:-}" ]]; 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
@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" \
pools Proxmox : un par tenant, dérivé de l'index (D-37, P28) Onze des quatorze serveurs portent le même nom court chez Chezlepro et chez Technolibre. Vérifié un par un, ce n'est pas un problème technique : tout le reste dérive du seed et diverge (10.27.19.21 contre 10.21.19.21, VMID 117402101 contre 111402101, VLAN 1174 contre 1114, deux domaines internes), et rien n'est indexé sur le nom court — les opérations Proxmox portent toutes un vmid, les certificats un FQDN, et client_backup_repo vise backup-01.{{ domaine_interne }}. Le coût est humain : la console Proxmox affiche le nom, et deux infra-pki-01 y sont indiscernables à l'œil. Le VMID porte le tenant, encore faut-il connaître le codage. Un pool par tenant, dérivé du dossier d'instance et de l'index — déjà unique par P21, donc aucun registre de plus : Chezlepro-17, Technolibre-11. make devis-proxmox-pools rattrape la flotte existante (création du pool, puis affectation des VM actives). Les VM créées ensuite entrent d'elles-mêmes : make creer-vm dérive le pool par la même fonction et le passe à la création. Le playbook crée le pool au préalable — proxmox_kvm échoue sur un pool inconnu, et l'API ne sait pas changer le pool d'une VM existante ; c'est aussi pourquoi le rattrapage passe par les membres. P28 garde deux collisions : même nom de pool entre tenants, et surtout même VMID — une machine appartenant à deux tenants serait pire qu'une homonymie. Rien n'est renommé : les homonymes sont la preuve que la nomenclature est un vrai gabarit. Le devis ne lit pas le cluster, il dit l'état cible et non l'écart. 27 preuves OK, 0 échec. --syntax-check du playbook de clonage. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 15:15:14 -04:00
-e proxmox_clone_pool="$$(python3 scripts/devis_proxmox_pools.py --pool-actif)" \
); \
[[ -n "$(VMID_MODELE)" ]] && extra_vars+=( -e proxmox_clone_vmid_modele="$(VMID_MODELE)" ); \
[[ -n "$(NOEUD_PROXMOX)" ]] && extra_vars+=( -e proxmox_clone_noeud="$(NOEUD_PROXMOX)" ); \
[[ -n "$(STOCKAGE_PROXMOX)" ]] && extra_vars+=( -e proxmox_clone_stockage="$(STOCKAGE_PROXMOX)" ); \
[[ -n "$(FORMAT_DISQUE)" ]] && extra_vars+=( -e proxmox_clone_format="$(FORMAT_DISQUE)" ); \
[[ -n "$(CLONE_COMPLET)" ]] && extra_vars+=( -e proxmox_clone_complet="$(CLONE_COMPLET)" ); \
[[ -n "$(TAILLE_DISQUE)" ]] && extra_vars+=( -e proxmox_clone_taille_disque="$(TAILLE_DISQUE)" ); \
[[ -n "$(COEURS)" ]] && extra_vars+=( -e proxmox_clone_coeurs="$(COEURS)" ); \
[[ -n "$(MEMOIRE)" ]] && extra_vars+=( -e proxmox_clone_memoire="$(MEMOIRE)" ); \
[[ -n "$(DISQUE_PROXMOX)" ]] && extra_vars+=( -e proxmox_clone_disque="$(DISQUE_PROXMOX)" ); \
[[ -n "$(DNS)" ]] && extra_vars+=( -e proxmox_clone_dns="$(DNS)" ); \
[[ -n "$(CIUSER)" ]] && extra_vars+=( -e proxmox_clone_ciuser="$(CIUSER)" ); \
[[ -n "$(CLE_SSH_PUBLIQUE)" ]] && extra_vars+=( -e proxmox_clone_cle_publique_fichier="$(CLE_SSH_PUBLIQUE)" ); \
[[ -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 [[ -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:-}" ]]; 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
@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"; \
$(MAKE) cloner-vm \
HOTE="$(HOTE)" \
VMID="$$SETOPS_VMID" \
ADRESSE_IP="$$SETOPS_IP" \
CIDR="$$SETOPS_CIDR" \
PASSERELLE="$$SETOPS_PASSERELLE" \
VLAN="$$SETOPS_VLAN" \
réseau : le VNet d'une VM se dérive, un hyperviseur a plusieurs pattes (D-55 à D-59) Le pont n'était pas seulement non portable, il était faux. proxmox_clone_pont faisait naître les VM sur vmbr1 avec une étiquette VLAN — l'ancien monde. En SDN une VM appartient à son VNet ; c'est ce qu'il a fallu corriger à la main sur infra-pki-01, et les treize suivantes auraient suivi. deriver_nomenclature() expose désormais la zone de sécurité, instancier en dérive proxmox_pont et une étiquette VIDE — le VNet porte déjà le tag, en poser un second donnerait un double étiquetage. La chaîne va jusqu'à make creer-vm : SETOPS_PONT='t11appl', SETOPS_VLAN=''. Trois pièges. Un doublon dans le Makefile passait PONT_PROXMOX deux fois dans la même cible, la seconde vide aurait écrasé la valeur dérivée. Un repli naïf sur proxmox_vlan aurait fait revenir l'étiquette en SDN : le repli ne s'applique que si la clé est ABSENTE, jamais si elle est présente et vide. Et le test unitaire est tombé, à raison — il couvre maintenant cette distinction. D-55 : le dépôt réseau porte le contrat entre l'Alliance et ses hébergeurs, et abstrait le matériel en encapsulant chaque tenant dans sa zone EVPN. Mesuré : un tenant est à deux valeurs de la portabilité complète (noeud, stockage). D-57 : l'interface sysadmin d'un hyperviseur (vmbr0, 10.0.0.41/.43/.47) n'a pas de route par défaut ; celle-ci vit sur vlan40, vers la frontière. On n'atteint l'administration que depuis son propre domaine de diffusion. Ça tranche la question de la sortie des nœuds laissée ouverte ce matin — option A, mais sur une interface dédiée, ce qui lève l'objection qui la bloquait. D-58 : un hôte déclare par quelle interface (`via`) chaque réseau lui arrive ; le devis en dérive un port par interface et son type — trunk 11,40 sur bond3, accès VLAN 10 sur vmbr0. Sans ça, ajouter le VLAN 10 le remettait sur le trunk du transport, soit le domaine qu'on venait d'en sortir. D-59 : un VLAN qui ne porte que des adresses d'hôte n'a pas besoin de pont. Régression créée puis corrigée : le modèle public, qui ne déclare aucun hyperviseur, n'émettait plus rien pour ce port. Il émet maintenant tout l'underlay en disant que c'est un repli. 30 preuves OK, 4 tests unitaires. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 16:20:56 -04:00
PONT_PROXMOX="$${SETOPS_PONT:-$(PONT_PROXMOX)}" \
STOCKAGE_PROXMOX="$${SETOPS_STOCKAGE:-$(STOCKAGE_PROXMOX)}" \
TAILLE_DISQUE="$${SETOPS_DISQUE:-$(TAILLE_DISQUE)}" \
COEURS="$${SETOPS_COEURS:-$(COEURS)}" \
MEMOIRE="$${SETOPS_MEMOIRE:-$(MEMOIRE)}" \
NOEUD_PROXMOX="$${SETOPS_NOEUD:-$(NOEUD_PROXMOX)}" \
VMID_MODELE="$(VMID_MODELE)" \
FORMAT_DISQUE="$(FORMAT_DISQUE)" \
DISQUE_PROXMOX="$(DISQUE_PROXMOX)" \
DNS="$${SETOPS_DNS:-$(DNS)}" \
DHCP="$(DHCP)" \
CIUSER="$(CIUSER)" \
CLE_SSH_PUBLIQUE="$(CLE_SSH_PUBLIQUE)" \
DEMARRER="$(DEMARRER)" \
CLONE_COMPLET="$(CLONE_COMPLET)"
@# `creer-vm` rend une VM PRETE, pas seulement demarree : sans cette attente,
@# enchainer `creer-vm` puis `deployer` echoue presque toujours sur une machine
@# neuve. C'est ce qui separe une suite de commandes d'une reconstruction.
@if [[ "$(ATTENDRE)" != "false" ]]; then \
$(MAKE) --no-print-directory _attendre-hote LIMITE="$(HOTE)"; \
fi
inventaire-verifier: ansible-runtime _instance-requise
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:
python3 scripts/serveurs.py lister
serveurs-verifier:
python3 scripts/serveurs.py verifier
serveurs-bootstrap:
python3 scripts/serveurs.py bootstrap
.PHONY: instancier instancier-appliquer
instancier: _instance-requise
python3 scripts/instancier.py generer
python3 scripts/instancier.py comparer
instancier-appliquer: _instance-requise
python3 scripts/instancier.py appliquer $(if $(FORCE),--force)
bases:
python3 scripts/bases_donnees.py lister
bases-verifier:
python3 scripts/bases_donnees.py verifier
domaines:
python3 scripts/domaines.py lister
domaines-verifier:
python3 scripts/domaines.py verifier
applications:
python3 scripts/applications.py lister
applications-verifier:
python3 scripts/applications.py verifier
applications-bootstrap:
python3 scripts/applications.py bootstrap
inventaire-lister: ansible-runtime
ansible-inventory -i $(FICHIER_INVENTAIRE) --list
inventaire-graphe: ansible-runtime
ansible-inventory -i $(FICHIER_INVENTAIRE) --graph
inventaire-hote: ansible-runtime
@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:
$(MAKE) inventaire-graphe FICHIER_INVENTAIRE="$(INVENTAIRE_LAB)"
inventaire-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
ansible -i $(INVENTAIRE_LAB) $(GROUPE_MODELE) -m ping -e ansible_become=false
_verifier-privileges-modele: ansible-runtime
ansible -i $(INVENTAIRE_LAB) $(GROUPE_MODELE) -b -m command -a "whoami"
preparer-modele: ansible-runtime _verifier-acces-modele _verifier-privileges-modele
ansible-playbook -i $(INVENTAIRE_LAB) $(PLAYBOOK_PREPARER_MODELE)
verifier-modele: ansible-runtime
ansible-playbook -i $(INVENTAIRE_LAB) $(PLAYBOOK_VERIFIER_MODELE)
nettoyer-modele: ansible-runtime
@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_LAB) $(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
ansible -i $(INVENTAIRE_PRODUCTION) $(LIMITE) -m setup -a "filter=ansible_distribution*"
verifier-hote: ansible-runtime
ansible-playbook -i $(INVENTAIRE_PRODUCTION) $(PLAYBOOK_VERIFIER_HOTE) $(OPTIONS_PLAYBOOK)