Compare commits
4 commits
main
...
essai_inte
| Author | SHA1 | Date | |
|---|---|---|---|
| acc3f83574 | |||
| a6907fed19 | |||
| 1cb85094ea | |||
| bff1396af3 |
239 changed files with 1474 additions and 16955 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -9,9 +9,6 @@ facts_cache/
|
||||||
**/vault.yml
|
**/vault.yml
|
||||||
# Underlay = fabric physique de l'operateur (cluster-global) ; gabarit public seul.
|
# Underlay = fabric physique de l'operateur (cluster-global) ; gabarit public seul.
|
||||||
/underlay.yml
|
/underlay.yml
|
||||||
# Contexte actif de CETTE machine (`site:<depot>` ou `locataire:<depot>`) : propre a chaque
|
|
||||||
# poste et a chaque runner, jamais versionne. Voir scripts/contexte.py.
|
|
||||||
/contexte
|
|
||||||
*.secret
|
*.secret
|
||||||
*.pem
|
*.pem
|
||||||
*.key
|
*.key
|
||||||
|
|
|
||||||
3326
CHANGELOG.md
3326
CHANGELOG.md
File diff suppressed because it is too large
Load diff
21
CLAUDE.md
21
CLAUDE.md
|
|
@ -4,3 +4,24 @@
|
||||||
|
|
||||||
`AGENTS.md` est la **source d'autorité unique** de ce dépôt. Claude Code doit le lire
|
`AGENTS.md` est la **source d'autorité unique** de ce dépôt. Claude Code doit le lire
|
||||||
et le respecter intégralement avant toute modification.
|
et le respecter intégralement avant toute modification.
|
||||||
|
|
||||||
|
En cas de contradiction entre `CLAUDE.md` et `AGENTS.md`, **suivre `AGENTS.md`**.
|
||||||
|
|
||||||
|
Ce fichier est volontairement mince : il ne redéclare pas la doctrine (template, SSH,
|
||||||
|
pare-feu, cloud-init, handlers, Makefile, groupes…). Toute cette doctrine vit dans
|
||||||
|
`AGENTS.md`, pour éviter la duplication et les dérives qu'elle provoque.
|
||||||
|
|
||||||
|
## Les cinq règles absolues
|
||||||
|
|
||||||
|
1. **Un seul agent IA** travaille dans ce dépôt à la fois (Codex *ou* Claude, jamais les deux).
|
||||||
|
2. **`instance/inventories/*/hosts.yml` est un artefact GÉNÉRÉ** depuis le plan
|
||||||
|
(`instance/plan/`). On l'édite jamais à la main : on édite le plan, puis
|
||||||
|
`make instancier` → `make instancier-appliquer`.
|
||||||
|
3. **Aucun secret en clair** (mots de passe, clés privées, tokens, certificats) — Vault
|
||||||
|
ou stockage hors dépôt uniquement.
|
||||||
|
4. **Toute action destructive ou risquée exige une confirmation explicite** (ex.
|
||||||
|
`CONFIRMER=true`, `confirm_destructive_action: true`). Sans elle, refuser l'exécution.
|
||||||
|
5. **Rien n'est « prêt » sans validation.** Au minimum `--syntax-check` du playbook
|
||||||
|
touché, `ansible-lint` si disponible, et l'entrée `CHANGELOG.md` correspondante.
|
||||||
|
|
||||||
|
Le détail de chacune de ces règles, et tout le reste, est dans `AGENTS.md`.
|
||||||
|
|
|
||||||
215
Makefile
215
Makefile
|
|
@ -21,16 +21,7 @@ export SETOPS_INSTANCE ?= instance
|
||||||
#
|
#
|
||||||
# `?=` : une valeur deja posee dans l'environnement gagne — la GUI et les runners peuvent
|
# `?=` : 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.
|
# 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)
|
||||||
# VIDE, ON N'EXPORTE RIEN (2026-10-04). Sur un clone sans aucune cle (la CI de la forge),
|
|
||||||
# `voutes.py` ne rend rien, et l'export posait quand meme `ANSIBLE_VAULT_IDENTITY_LIST=`.
|
|
||||||
# Ansible lit cette chaine vide comme un FICHIER de mot de passe, la resout en repertoire
|
|
||||||
# courant et refuse (« can not be a directory ») : `make ci` mourait a sa premiere commande
|
|
||||||
# sur un clone nu. Le poste a toujours des cles, il ne pouvait pas le voir.
|
|
||||||
SETOPS_VOUTES_IDENTITES := $(shell python3 scripts/voutes.py identites 2>/dev/null)
|
|
||||||
ifneq ($(strip $(SETOPS_VOUTES_IDENTITES)),)
|
|
||||||
export ANSIBLE_VAULT_IDENTITY_LIST ?= $(SETOPS_VOUTES_IDENTITES)
|
|
||||||
endif
|
|
||||||
# Inventaire de l'instance : un seul par instance dans le modèle « séparation par
|
# Inventaire de l'instance : un seul par instance dans le modèle « séparation par
|
||||||
# instance ». Détection rétro-compatible : principal > production > lab.
|
# instance ». Détection rétro-compatible : principal > production > lab.
|
||||||
# Surchargeable : make … SETOPS_INVENTAIRE=chemin/hosts.yml
|
# Surchargeable : make … SETOPS_INVENTAIRE=chemin/hosts.yml
|
||||||
|
|
@ -317,11 +308,6 @@ test: ## Lance les tests unitaires (derivation de nomenclature et d'inventaire)
|
||||||
python3 scripts/tests/test_inventory_host.py
|
python3 scripts/tests/test_inventory_host.py
|
||||||
python3 scripts/tests/test_raser.py
|
python3 scripts/tests/test_raser.py
|
||||||
python3 scripts/tests/test_raser_resultat.py
|
python3 scripts/tests/test_raser_resultat.py
|
||||||
python3 scripts/tests/test_temoins.py
|
|
||||||
python3 scripts/tests/test_genome_colis.py
|
|
||||||
python3 scripts/tests/test_verifier_depot.py
|
|
||||||
python3 scripts/tests/test_apps_nextcloud.py
|
|
||||||
python3 scripts/tests/test_verrou_runner.py
|
|
||||||
python3 scripts/tests/test_adressage_derive.py
|
python3 scripts/tests/test_adressage_derive.py
|
||||||
python3 scripts/tests/test_gui_intrants.py
|
python3 scripts/tests/test_gui_intrants.py
|
||||||
python3 scripts/tests/test_runbooks.py
|
python3 scripts/tests/test_runbooks.py
|
||||||
|
|
@ -331,16 +317,6 @@ test: ## Lance les tests unitaires (derivation de nomenclature et d'inventaire)
|
||||||
python3 scripts/tests/test_fraicheur_icinga.py
|
python3 scripts/tests/test_fraicheur_icinga.py
|
||||||
python3 scripts/tests/test_sondes_conditionnelles.py
|
python3 scripts/tests/test_sondes_conditionnelles.py
|
||||||
python3 scripts/tests/test_sondes_syntaxe.py
|
python3 scripts/tests/test_sondes_syntaxe.py
|
||||||
python3 scripts/tests/test_empreinte_plancher.py
|
|
||||||
python3 scripts/tests/test_expositions_sans_port.py
|
|
||||||
python3 scripts/tests/test_proxmox_fw_externe.py
|
|
||||||
python3 scripts/tests/test_libelles_parefeu.py
|
|
||||||
python3 scripts/tests/test_sonde_tcp.py
|
|
||||||
python3 scripts/tests/test_restauration.py
|
|
||||||
python3 scripts/tests/test_repertoires_partages.py
|
|
||||||
python3 scripts/tests/test_frontiere_refus.py
|
|
||||||
python3 scripts/tests/test_contexte.py
|
|
||||||
python3 scripts/tests/test_appels_locataire.py
|
|
||||||
|
|
||||||
# LA SONDE QUI RAPPORTE CE QUI DISTINGUE (2026-08-28). Trois faux diagnostics en une
|
# 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
|
# journee, tous dus a l'instrument : `curl` et `bash /dev/tcp` ecrasent « la machine
|
||||||
|
|
@ -560,12 +536,8 @@ hote-afficher: ansible-runtime ## Affiche tout ce que le plan derive pour un hot
|
||||||
python3 scripts/inventory_host.py --inventaire $(FICHIER_INVENTAIRE) afficher --hote $(HOTE)
|
python3 scripts/inventory_host.py --inventaire $(FICHIER_INVENTAIRE) afficher --hote $(HOTE)
|
||||||
|
|
||||||
appliquer: ansible-runtime ## Applique un groupe a la flotte — GROUPE=<groupe>
|
appliquer: ansible-runtime ## Applique un groupe a la flotte — GROUPE=<groupe>
|
||||||
@# `-z "$(GROUPE)"` NE POUVAIT JAMAIS SE DECLENCHER (2026-10-03) : `GROUPE ?=
|
@if [[ -z "$(GROUPE)" ]]; then \
|
||||||
@# $(GROUPE_DEBIAN)` lui donne toujours une valeur. Une commande coupee par un retour
|
printf '%s\n' 'Refus: relancer avec GROUPE=nom_groupe.'; \
|
||||||
@# a la ligne (`make appliquer` seul) a ainsi applique `serveur_debian` a toute la flotte
|
|
||||||
@# de Technolibre. On refuse donc un groupe que l'operateur n'a pas DONNE lui-meme.
|
|
||||||
@if [[ "$(origin GROUPE)" == "file" || -z "$(GROUPE)" ]]; then \
|
|
||||||
printf '%s\n' 'Refus: relancer avec GROUPE=nom_groupe (aucun groupe par defaut ici).'; \
|
|
||||||
exit 2; \
|
exit 2; \
|
||||||
fi
|
fi
|
||||||
@if [[ ! -f "$(DOSSIER_PLAYBOOKS_GROUPES)/$(GROUPE).yml" ]]; then \
|
@if [[ ! -f "$(DOSSIER_PLAYBOOKS_GROUPES)/$(GROUPE).yml" ]]; then \
|
||||||
|
|
@ -573,9 +545,7 @@ appliquer: ansible-runtime ## Applique un groupe a la flotte — GROUPE=<groupe>
|
||||||
exit 2; \
|
exit 2; \
|
||||||
fi
|
fi
|
||||||
python3 scripts/inventory_host.py --inventaire $(INVENTAIRE_PRODUCTION) --dependances $(FICHIER_DEPENDANCES) verifier-dependances-groupe --groupe $(GROUPE)
|
python3 scripts/inventory_host.py --inventaire $(INVENTAIRE_PRODUCTION) --dependances $(FICHIER_DEPENDANCES) verifier-dependances-groupe --groupe $(GROUPE)
|
||||||
@# `ARGS` comme chez `site-appliquer` (2026-10-03) : simuler (`ARGS="--check --diff"`)
|
ansible-playbook -i $(INVENTAIRE_PRODUCTION) "$(DOSSIER_PLAYBOOKS_GROUPES)/$(GROUPE).yml" --limit '$(GROUPE):&$(GROUPE_HOTES_ACTIFS)'
|
||||||
@# avant d'appliquer doit etre possible par la meme porte que l'application elle-meme.
|
|
||||||
ansible-playbook -i $(INVENTAIRE_PRODUCTION) "$(DOSSIER_PLAYBOOKS_GROUPES)/$(GROUPE).yml" --limit '$(GROUPE):&$(GROUPE_HOTES_ACTIFS)' $(ARGS)
|
|
||||||
|
|
||||||
# ATTENDRE PLUTOT QUE CONSTATER (2026-08-27). `deployer` faisait un `ping` unique
|
# 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
|
# (`_verifier-acces-hote`). Depuis que `creer-vm` ne guette plus le SSH — il prouve la
|
||||||
|
|
@ -617,7 +587,7 @@ site: ansible-runtime ## Regenere playbooks/site.yml depuis les couches et le gr
|
||||||
site-verifier: ## Verifie que playbooks/site.yml correspond aux couches declarees
|
site-verifier: ## Verifie que playbooks/site.yml correspond aux couches declarees
|
||||||
python3 scripts/orchestrer.py verifier
|
python3 scripts/orchestrer.py verifier
|
||||||
|
|
||||||
.PHONY: flux flux-verifier fiches-site-deposer
|
.PHONY: flux flux-verifier
|
||||||
cles-recenser: ## Montre ce qui n'existe QUE sur ce poste (sans rien ecrire)
|
cles-recenser: ## Montre ce qui n'existe QUE sur ce poste (sans rien ecrire)
|
||||||
python3 scripts/exporter_cles.py --recenser
|
python3 scripts/exporter_cles.py --recenser
|
||||||
|
|
||||||
|
|
@ -753,8 +723,8 @@ proxmox-fw-activer-vm: ansible-runtime ## Active le pare-feu d'UNE VM en le prou
|
||||||
|
|
||||||
.PHONY: proxmox-fw-eprouver proxmox-fw-activer-vm
|
.PHONY: proxmox-fw-eprouver proxmox-fw-activer-vm
|
||||||
.PHONY: placement-plan sdn-plan sdn-appliquer
|
.PHONY: placement-plan sdn-plan sdn-appliquer
|
||||||
placement-plan: ansible-runtime ## Le noeud, stockage, pont et gabarit du tenant existent-ils sur ce cluster ? (aucune ecriture) [TENANT=<depot>]
|
placement-plan: ansible-runtime ## Le noeud, stockage, pont et gabarit du tenant existent-ils sur ce cluster ? (aucune ecriture)
|
||||||
python3 scripts/devis_placement.py $(if $(TENANT),--locataire $(TENANT))
|
python3 scripts/devis_placement.py
|
||||||
|
|
||||||
sdn-plan: ansible-runtime ## Ecart entre le SDN EVPN (+ sortie des VRF) et son devis (aucune ecriture)
|
sdn-plan: ansible-runtime ## Ecart entre le SDN EVPN (+ sortie des VRF) et son devis (aucune ecriture)
|
||||||
python3 scripts/appliquer_sdn.py
|
python3 scripts/appliquer_sdn.py
|
||||||
|
|
@ -877,17 +847,6 @@ site-intrants: ## Les intrants que le SITE monte expose a ses locataires (derive
|
||||||
site-intrants-verifier: ## Le locataire monte suit-il encore son site ? (aucune ecriture)
|
site-intrants-verifier: ## Le locataire monte suit-il encore son site ? (aucune ecriture)
|
||||||
python3 scripts/site_intrants.py --verifier
|
python3 scripts/site_intrants.py --verifier
|
||||||
|
|
||||||
face-reseau-publier: ## Chaque locataire publie sa face reseau pour son site (a commiter chez lui, apres `make flux`)
|
|
||||||
@# LE LOCATAIRE PUBLIE, LE SITE LIT (2026-10-05). Le site lisait les fichiers internes de ses
|
|
||||||
@# locataires ; il lit desormais `face-reseau.yml`. P92 refuse une face perimee.
|
|
||||||
python3 scripts/contexte.py --publier-faces
|
|
||||||
|
|
||||||
fiches-site-deposer: ## Le site depose chez chacun de ses locataires la fiche qu'il lui destine (a commiter chez eux)
|
|
||||||
@# LE SITE DEPOSE, LE LOCATAIRE LIT (2026-10-05). Le runner d'un locataire n'a pas le depot
|
|
||||||
@# de son site : ce que le site lui attribue et lui offre lui parvient par ce fichier,
|
|
||||||
@# `fiche-site.yml`, que l'instancier lit. P91 refuse une fiche perimee.
|
|
||||||
python3 scripts/contexte.py --deposer-fiches
|
|
||||||
|
|
||||||
.PHONY: vpn-admin-plan vpn-admin-appliquer
|
.PHONY: vpn-admin-plan vpn-admin-appliquer
|
||||||
vpn-admin-plan: ## Acces WireGuard des admins : ce que la frontiere porte face au plan (aucune ecriture)
|
vpn-admin-plan: ## Acces WireGuard des admins : ce que la frontiere porte face au plan (aucune ecriture)
|
||||||
python3 scripts/vpn_admin.py plan
|
python3 scripts/vpn_admin.py plan
|
||||||
|
|
@ -937,39 +896,6 @@ flux-verifier: ## Verifie que le registre des flux correspond aux meta/flux.yml
|
||||||
valider: ansible-runtime ## Passe la recette de validation sur la flotte
|
valider: ansible-runtime ## Passe la recette de validation sur la flotte
|
||||||
ansible-playbook -i $(INVENTAIRE_PRODUCTION) playbooks/valider.yml
|
ansible-playbook -i $(INVENTAIRE_PRODUCTION) playbooks/valider.yml
|
||||||
|
|
||||||
.PHONY: restauration-etat restauration-renoncer sauvegarder-maintenant temoins-etat
|
|
||||||
sauvegarder-maintenant: ansible-runtime ## Depose l'etat de chaque noeud TOUT DE SUITE (avant de raser) — HOTE=<hote> optionnel
|
|
||||||
@set -e; $(VAULT_UNE_FOIS) \
|
|
||||||
ansible-playbook -i $(INVENTAIRE_PRODUCTION) playbooks/backup/restauration.yml \
|
|
||||||
-e restauration_action=sauvegarder $(if $(HOTE),--limit "$(HOTE)")
|
|
||||||
|
|
||||||
# LA RESTAURATION DANS LA RECONSTRUCTION (2026-09-30) — la remise elle-meme n'a pas de
|
|
||||||
# cible : chaque role proprietaire la fait au deploiement. Ces deux cibles-ci servent a
|
|
||||||
# VOIR ce qui attend, et a ECARTER un etat qu'on ne veut pas remettre.
|
|
||||||
restauration-etat: ansible-runtime ## Ce qui attend dans le depot, et ce que chaque jeu est devenu — HOTE=<hote> optionnel
|
|
||||||
@set -e; $(VAULT_UNE_FOIS) \
|
|
||||||
ansible-playbook -i $(INVENTAIRE_PRODUCTION) playbooks/backup/restauration.yml \
|
|
||||||
$(if $(HOTE),--limit "$(HOTE)")
|
|
||||||
|
|
||||||
# LES TEMOINS (2026-10-07) — l'etat remis est-il celui d'avant ? Compare l'etat vivant a
|
|
||||||
# l'instantane etiquete au rasage (`sauvegarder-maintenant`). `SOURCE=candidat` compare au
|
|
||||||
# dernier instantane d'avant la naissance du noeud, faute d'etiquette. Lecture seule.
|
|
||||||
temoins-etat: ansible-runtime ## L'etat vivant est-il celui d'avant le rasage ? (lecture seule) — HOTE= SOURCE=candidat optionnels
|
|
||||||
@set -e; $(VAULT_UNE_FOIS) \
|
|
||||||
ansible-playbook -i $(INVENTAIRE_PRODUCTION) playbooks/backup/restauration.yml \
|
|
||||||
-e restauration_action=temoins $(if $(SOURCE),-e restauration_source="$(SOURCE)") \
|
|
||||||
$(if $(HOTE),--limit "$(HOTE)")
|
|
||||||
|
|
||||||
restauration-renoncer: ansible-runtime ## Ecarte l'etat anterieur d'un jeu, sans le remettre — HOTE= JEU= CONFIRMER=true
|
|
||||||
@set -e; \
|
|
||||||
if [[ "$(CONFIRMER)" != "true" || -z "$(HOTE)" || -z "$(JEU)" ]]; then \
|
|
||||||
printf '%s\n' 'Refus: ECARTER un etat anterieur (il sortira de la retention). HOTE=, JEU= et CONFIRMER=true requis.'; \
|
|
||||||
exit 2; \
|
|
||||||
fi; \
|
|
||||||
$(VAULT_UNE_FOIS) \
|
|
||||||
ansible-playbook -i $(INVENTAIRE_PRODUCTION) playbooks/backup/restauration.yml \
|
|
||||||
--limit "$(HOTE)" -e restauration_action=renoncer -e restauration_jeu="$(JEU)"
|
|
||||||
|
|
||||||
.PHONY: schema
|
.PHONY: schema
|
||||||
schema: ## Regenere docs/audit/schema-plan.json depuis les registres et les validateurs
|
schema: ## Regenere docs/audit/schema-plan.json depuis les registres et les validateurs
|
||||||
python3 scripts/schema_plan.py
|
python3 scripts/schema_plan.py
|
||||||
|
|
@ -1154,23 +1080,14 @@ define VAULT_UNE_FOIS
|
||||||
endef
|
endef
|
||||||
|
|
||||||
|
|
||||||
# LE SITE MATERIALISE UN LOCATAIRE NOMME, D'APRES SA FACE (2026-10-05, docs/conception-contextes.md,
|
flotte-creer: _instance-requise ## Cree les VM manquantes de la flotte depuis le plan — PARALLELE=n
|
||||||
# materialisation M2). Avec `TENANT=<depot>`, la liste des machines et leurs parametres de clonage
|
|
||||||
# viennent de `face-reseau.yml` que le locataire publie — rendus a l'octet pres comme
|
|
||||||
# `parametres-proxmox` (P93) — et aucune instance n'est montee. Sans `TENANT`, l'ancienne lecture
|
|
||||||
# de l'instance montee (le poste, le laboratoire).
|
|
||||||
LISTE_ACTIFS = $(if $(TENANT),python3 scripts/contexte.py --hotes-actifs $(TENANT),python3 scripts/inventory_host.py --inventaire $(INVENTAIRE_PRODUCTION) lister-actifs)
|
|
||||||
PARAMS_CLONAGE = $(if $(TENANT),python3 scripts/contexte.py --parametres-clonage $(TENANT) --hote,python3 scripts/inventory_host.py --inventaire $(INVENTAIRE_PRODUCTION) parametres-proxmox --hote)
|
|
||||||
INSTANCE_OU_LOCATAIRE = $(if $(TENANT),,_instance-requise)
|
|
||||||
|
|
||||||
flotte-creer: $(INSTANCE_OU_LOCATAIRE) ## Cree les VM manquantes de la flotte depuis le plan — PARALLELE=n
|
|
||||||
@set -e; \
|
@set -e; \
|
||||||
if [[ "$(CONFIRMER)" != "true" ]]; then \
|
if [[ "$(CONFIRMER)" != "true" ]]; then \
|
||||||
printf '%s\n' 'Refus: creation de TOUTES les VM actives du plan (clone Proxmox).'; \
|
printf '%s\n' 'Refus: creation de TOUTES les VM actives du plan (clone Proxmox).'; \
|
||||||
printf '%s\n' 'Relancer avec CONFIRMER=true.'; \
|
printf '%s\n' 'Relancer avec CONFIRMER=true.'; \
|
||||||
exit 2; \
|
exit 2; \
|
||||||
fi; \
|
fi; \
|
||||||
hotes="$$($(LISTE_ACTIFS))"; \
|
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; \
|
if [[ -z "$$hotes" ]]; then printf '%s\n' 'Refus: aucun hote actif dans le plan.'; exit 2; fi; \
|
||||||
par="$(PARALLELE)"; par="$${par:-4}"; \
|
par="$(PARALLELE)"; par="$${par:-4}"; \
|
||||||
tmp="$$(mktemp -d)"; trap 'rm -rf "$$tmp"' EXIT; \
|
tmp="$$(mktemp -d)"; trap 'rm -rf "$$tmp"' EXIT; \
|
||||||
|
|
@ -1179,7 +1096,7 @@ flotte-creer: $(INSTANCE_OU_LOCATAIRE) ## Cree les VM manquantes de la flotte de
|
||||||
n=0; \
|
n=0; \
|
||||||
for h in $$hotes; do \
|
for h in $$hotes; do \
|
||||||
printf '\n=== Creation VM: %s ===\n' "$$h"; \
|
printf '\n=== Creation VM: %s ===\n' "$$h"; \
|
||||||
( $(MAKE) creer-vm HOTE="$$h" $(if $(TENANT),TENANT="$(TENANT)") > "$$tmp/$$h.log" 2>&1; printf '%s' "$$?" > "$$tmp/$$h.rc" ) & \
|
( $(MAKE) creer-vm HOTE="$$h" > "$$tmp/$$h.log" 2>&1; printf '%s' "$$?" > "$$tmp/$$h.rc" ) & \
|
||||||
n=$$((n+1)); \
|
n=$$((n+1)); \
|
||||||
if (( n >= par )); then wait -n || true; n=$$((n-1)); fi; \
|
if (( n >= par )); then wait -n || true; n=$$((n-1)); fi; \
|
||||||
done; \
|
done; \
|
||||||
|
|
@ -1194,13 +1111,6 @@ flotte-creer: $(INSTANCE_OU_LOCATAIRE) ## Cree les VM manquantes de la flotte de
|
||||||
if (( echec )); then printf '\nAu moins une VM n a pas ete creee — rien ne continue.\n'; exit 2; fi; \
|
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'
|
printf '\nToutes les VM actives sont creees.\n'
|
||||||
|
|
||||||
locataire-creer: ## Le SITE cree les VM d'un locataire d'apres sa face reseau — TENANT=<depot> CONFIRMER=true [PARALLELE=n]
|
|
||||||
@if [[ -z "$(TENANT)" ]]; then printf '%s\n' 'Refus: TENANT=<depot du locataire> requis.'; exit 2; fi
|
|
||||||
@$(MAKE) --no-print-directory flotte-creer TENANT="$(TENANT)" CONFIRMER="$(CONFIRMER)" PARALLELE="$(PARALLELE)"
|
|
||||||
locataire-raser: ## DESTRUCTIF : le SITE rase les VM d'un locataire d'apres sa face reseau — TENANT=<depot> CONFIRMER=true INSTANCE=<nom court> [HOTE=h]
|
|
||||||
@if [[ -z "$(TENANT)" ]]; then printf '%s\n' 'Refus: TENANT=<depot du locataire> requis.'; exit 2; fi
|
|
||||||
@$(MAKE) --no-print-directory raser TENANT="$(TENANT)" CONFIRMER="$(CONFIRMER)" INSTANCE="$(INSTANCE)" HOTE="$(HOTE)"
|
|
||||||
|
|
||||||
# --- LES MACHINES DU SITE -----------------------------------------------------
|
# --- 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
|
# UN SITE N'EST PAS UN PLAN. Un tenant se derive de son `index` ; un site n'en a pas et
|
||||||
|
|
@ -1222,10 +1132,6 @@ site-inventaire: ## L'inventaire dynamique du site, tel qu'Ansible le voit
|
||||||
|
|
||||||
.PHONY: site-appliquer
|
.PHONY: site-appliquer
|
||||||
site-appliquer: ## Applique un role aux machines du site — GROUPE=<serveur_ops_site|...>
|
site-appliquer: ## Applique un role aux machines du site — GROUPE=<serveur_ops_site|...>
|
||||||
@if [[ "$(origin GROUPE)" == "file" || -z "$(GROUPE)" ]]; then \
|
|
||||||
printf '%s\n' 'Refus: relancer avec GROUPE=nom_groupe (aucun groupe par defaut ici).'; \
|
|
||||||
exit 2; \
|
|
||||||
fi
|
|
||||||
@# `GROUPE` a un defaut global (GROUPE_DEBIAN) : tester s'il est VIDE ne protege de
|
@# `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 :
|
@# 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
|
@# lancer un playbook de TENANT contre l'inventaire du SITE. Ansible n'y trouve aucun
|
||||||
|
|
@ -1466,8 +1372,8 @@ _attendre-flotte: ansible-runtime
|
||||||
printf 'Flotte joignable.\n'
|
printf 'Flotte joignable.\n'
|
||||||
|
|
||||||
.PHONY: reconstruire
|
.PHONY: reconstruire
|
||||||
raser: ansible-runtime $(INSTANCE_OU_LOCATAIRE) ## DESTRUCTIF : detruit les VM derivees du plan — exige CONFIRMER=true ET INSTANCE=<nom> [TENANT=<depot>]
|
raser: ansible-runtime _instance-requise ## DESTRUCTIF : detruit les VM derivees du plan — exige CONFIRMER=true ET INSTANCE=<nom>
|
||||||
@python3 scripts/raser.py $(if $(TENANT),--locataire $(TENANT)) $(if $(INSTANCE),--instance $(INSTANCE)) $(if $(HOTE),--hote $(HOTE)) $(if $(filter true,$(CONFIRMER)),--confirmer)
|
@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/`
|
# `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
|
# est VIDE et le socle deploie nftables en `policy drop` SANS AUCUNE REGLE DERIVEE. La
|
||||||
|
|
@ -1488,70 +1394,8 @@ reconstruire: _instance-requise ## Reconstruit un ecosysteme depuis zero : VM pu
|
||||||
$(MAKE) flux; \
|
$(MAKE) flux; \
|
||||||
$(MAKE) flotte-creer CONFIRMER=true; \
|
$(MAKE) flotte-creer CONFIRMER=true; \
|
||||||
$(MAKE) _attendre-flotte; \
|
$(MAKE) _attendre-flotte; \
|
||||||
$(MAKE) monter-flotte CONFIRMER=true
|
$(MAKE) _amorcer-socle; \
|
||||||
|
$(MAKE) deployer-tout CONFIRMER=true
|
||||||
# MONTER UNE FLOTTE DONT LES VM EXISTENT DEJA (2026-09-30). C'est la seconde moitie de
|
|
||||||
# `reconstruire`, et TOUT ce que le runner d'un locataire peut en faire : il ne cree pas de VM
|
|
||||||
# (l'API du cluster est a l'hebergeur), il les recoit, armees, et les monte. Jusqu'ici cette
|
|
||||||
# sequence vivait dans un script ecrit a la main dans /tmp du runner — deux reconstructions
|
|
||||||
# l'ont portee, et aucune trace ne la gardait.
|
|
||||||
#
|
|
||||||
# `set -e` : la premiere etape en echec arrete tout. Chaque etape est horodatee, pour qu'un
|
|
||||||
# journal dise ou l'on en etait sans relire dix mille lignes d'Ansible. `valider` fait partie
|
|
||||||
# de la sequence : une flotte sans recette n'est pas montee, elle est deployee.
|
|
||||||
.PHONY: monter-flotte
|
|
||||||
monter-flotte: _instance-requise ## Monte une flotte deja creee : flux, AC et DNS, tout le reste, recette — CONFIRMER=true
|
|
||||||
@set -e; \
|
|
||||||
if [[ "$(CONFIRMER)" != "true" ]]; then \
|
|
||||||
printf '%s\n' 'Refus: MONTER la flotte (deploiement complet puis recette). Relancer avec CONFIRMER=true.'; \
|
|
||||||
exit 2; \
|
|
||||||
fi; \
|
|
||||||
etape() { printf '\n=== ETAPE %s %s\n' "$$1" "$$(date +%T)"; }; \
|
|
||||||
etape flux; $(MAKE) flux; \
|
|
||||||
etape _amorcer-socle; $(MAKE) _amorcer-socle; \
|
|
||||||
etape deployer-tout; $(MAKE) deployer-tout CONFIRMER=true; \
|
|
||||||
etape valider; $(MAKE) valider; \
|
|
||||||
printf '\n=== FLOTTE MONTEE %s\n' "$$(date +%T)"
|
|
||||||
|
|
||||||
# RECONSTRUIRE UN LOCATAIRE DE BOUT EN BOUT, DEPUIS LE POSTE (2026-09-30). Les deux
|
|
||||||
# reconstructions du 2026-09-30 passaient par cinq endroits et des commandes tapees a la
|
|
||||||
# main ; la sequence vit desormais dans `scripts/reconstruire_locataire.py` (voir son
|
|
||||||
# en-tete). `TENANT=` designe l'ecosysteme en toutes lettres : le nom qu'exige `raser` en est
|
|
||||||
# derive, pas redemande. L'armement marque une pause, sauf `ARMER=oui`. `DEPUIS=<etape>` reprend apres un
|
|
||||||
# arret. Le journal complet va dans `<locataire>/logs/`.
|
|
||||||
.PHONY: reconstruire-locataire
|
|
||||||
reconstruire-locataire: ## Rase et reconstruit un locataire, du site a la recette, etat remis — TENANT=OPS-X CONFIRMER=true
|
|
||||||
@if [[ -z "$(TENANT)" ]]; then \
|
|
||||||
printf '%s\n' 'Refus: TENANT=<OPS-X> requis (le dossier du locataire, en toutes lettres).'; \
|
|
||||||
exit 2; \
|
|
||||||
fi
|
|
||||||
python3 scripts/reconstruire_locataire.py --tenant "$(TENANT)" \
|
|
||||||
$(if $(filter true,$(CONFIRMER)),--confirmer) $(if $(DEPUIS),--depuis "$(DEPUIS)") \
|
|
||||||
$(if $(filter oui,$(ARMER)),--armer)
|
|
||||||
|
|
||||||
# LE PARE-FEU PROXMOX D'UNE FLOTTE RECONSTRUITE (2026-09-30). Une VM clonee nait sans les
|
|
||||||
# options de pare-feu : chaque reconstruction les perd. La procedure (une VM a la fois,
|
|
||||||
# matrice avant/apres, Icinga) etait rejouee par une boucle tapee a la main ; `--flotte` la
|
|
||||||
# porte, avec la regle qui la rend sure : arret au premier refus.
|
|
||||||
#
|
|
||||||
# PAR LES SONDES DEPUIS LE 2026-09-30 (`--sondes`) : la matrice prenait une vingtaine de
|
|
||||||
# minutes en fin de reconstruction. Chaque VM teste desormais chaque minute ses flux promis
|
|
||||||
# (sonde `connectivite`) ; on active tout d'un coup et on lit Icinga. La matrice reste pour
|
|
||||||
# le diagnostic : `python3 scripts/eprouver_parefeu.py --instance OPS-X --hote <vm> --plan`.
|
|
||||||
#
|
|
||||||
# DEPUIS LE POSTE : la matrice se joue avec les acces du LOCATAIRE, l'activation passe par
|
|
||||||
# le runner du SITE, seul a joindre l'API du cluster. Le poste est le seul a tenir les deux.
|
|
||||||
.PHONY: parefeu-verifier-flotte parefeu-activer-flotte
|
|
||||||
parefeu-verifier-flotte: ## La sonde `connectivite` est-elle saine sur chaque VM (prealable a l'activation) — INSTANCE=OPS-X
|
|
||||||
@if [[ -z "$(INSTANCE)" ]]; then printf '%s\n' 'Refus: INSTANCE=<OPS-X> requis.'; exit 2; fi
|
|
||||||
python3 scripts/eprouver_parefeu.py --instance "$(INSTANCE)" --flotte --sondes --plan
|
|
||||||
|
|
||||||
parefeu-activer-flotte: ## Active le pare-feu Proxmox de toutes les VM, juge par la sonde `connectivite` — INSTANCE=OPS-X CONFIRMER=true
|
|
||||||
@if [[ -z "$(INSTANCE)" || "$(CONFIRMER)" != "true" ]]; then \
|
|
||||||
printf '%s\n' 'Refus: INSTANCE=<OPS-X> et CONFIRMER=true requis (active le pare-feu de TOUTES les VM, une a une).'; \
|
|
||||||
exit 2; \
|
|
||||||
fi
|
|
||||||
python3 scripts/eprouver_parefeu.py --instance "$(INSTANCE)" --flotte --sondes --activer
|
|
||||||
|
|
||||||
# « Go ahead, make my day. » — LE bouton rouge : alias de reconstruire (Clint Eastwood).
|
# « 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.
|
# Cree toutes les VM puis deploie tout, en une commande. Garde CONFIRMER=true.
|
||||||
|
|
@ -1576,12 +1420,8 @@ _amorcer-socle: ansible-runtime _instance-requise
|
||||||
myDay: reconstruire ## Alias strict de `reconstruire` (meme cible, memes gardes)
|
myDay: reconstruire ## Alias strict de `reconstruire` (meme cible, memes gardes)
|
||||||
|
|
||||||
deployer-groupe: ## Deploie un seul groupe sur toute la flotte — GROUPE=<groupe>
|
deployer-groupe: ## Deploie un seul groupe sur toute la flotte — GROUPE=<groupe>
|
||||||
@# `-z "$(GROUPE)"` NE POUVAIT JAMAIS SE DECLENCHER (2026-10-03) : `GROUPE ?=
|
@if [[ -z "$(GROUPE)" ]]; then \
|
||||||
@# $(GROUPE_DEBIAN)` lui donne toujours une valeur. Une commande coupee par un retour
|
printf '%s\n' 'Refus: relancer avec GROUPE=nom_groupe.'; \
|
||||||
@# a la ligne (`make appliquer` seul) a ainsi applique `serveur_debian` a toute la flotte
|
|
||||||
@# de Technolibre. On refuse donc un groupe que l'operateur n'a pas DONNE lui-meme.
|
|
||||||
@if [[ "$(origin GROUPE)" == "file" || -z "$(GROUPE)" ]]; then \
|
|
||||||
printf '%s\n' 'Refus: relancer avec GROUPE=nom_groupe (aucun groupe par defaut ici).'; \
|
|
||||||
exit 2; \
|
exit 2; \
|
||||||
fi
|
fi
|
||||||
$(MAKE) appliquer GROUPE="$(GROUPE)"
|
$(MAKE) appliquer GROUPE="$(GROUPE)"
|
||||||
|
|
@ -1616,6 +1456,18 @@ verifier-deploiement: ansible-runtime ## Verifie l'etat de la flotte apres deplo
|
||||||
ansible-playbook -i $(INVENTAIRE_PRODUCTION) "$$playbook" --limit "$(HOTE)" --check --diff; \
|
ansible-playbook -i $(INVENTAIRE_PRODUCTION) "$$playbook" --limit "$(HOTE)" --check --diff; \
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# LE CLONAGE SE SERIALISE PAR STOCKAGE, et c'est la granularite de Proxmox
|
||||||
|
# lui-meme : il tient un verrou PAR STOCKAGE pendant l'allocation du disque. Deux
|
||||||
|
# clones simultanes sur le meme stockage se le disputent, et celui qui attend trop
|
||||||
|
# rend « clone failed: can't lock file '/var/lock/pve-manager/pve-storage-<nom>' -
|
||||||
|
# got timeout » — un message qui nomme le verrou, jamais le parallelisme qui l'a
|
||||||
|
# provoque. A deux VM rien ne parait ; a cinq, une creation sur cinq echoue, et la
|
||||||
|
# reconstruction s'arrete sans que rien ne dise pourquoi elle tenait a deux.
|
||||||
|
#
|
||||||
|
# LA CLE PORTE LE NOEUD ET LE STOCKAGE, donc deux stockages distincts clonent
|
||||||
|
# TOUJOURS en parallele : `flotte-creer PARALLELE=n` garde tout son sens, et seule
|
||||||
|
# la section qui contend est prise un a la fois, dans l'ordre ou la sequence du
|
||||||
|
# plan la demande.
|
||||||
cloner-vm: ansible-runtime ## Clone une VM depuis le gabarit dore — HOTE=<nom> VMID=<id>
|
cloner-vm: ansible-runtime ## Clone une VM depuis le gabarit dore — HOTE=<nom> VMID=<id>
|
||||||
@if [[ -z "$(HOTE)" || -z "$(VMID)" ]]; then \
|
@if [[ -z "$(HOTE)" || -z "$(VMID)" ]]; then \
|
||||||
printf '%s\n' 'Refus: relancer avec HOTE=nom VMID=id_clone.'; \
|
printf '%s\n' 'Refus: relancer avec HOTE=nom VMID=id_clone.'; \
|
||||||
|
|
@ -1649,7 +1501,7 @@ cloner-vm: ansible-runtime ## Clone une VM depuis le gabarit dore — HOTE=<nom>
|
||||||
-e proxmox_clone_nom="$(HOTE)" \
|
-e proxmox_clone_nom="$(HOTE)" \
|
||||||
-e proxmox_clone_vmid="$(VMID)" \
|
-e proxmox_clone_vmid="$(VMID)" \
|
||||||
-e proxmox_clone_ipconfig0="$$ipconfig" \
|
-e proxmox_clone_ipconfig0="$$ipconfig" \
|
||||||
-e proxmox_clone_pool="$(if $(POOL),$(POOL),$$(python3 scripts/devis_proxmox_pools.py $(if $(TENANT),--pool-du-locataire $(TENANT),--pool-actif)))" \
|
-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 "$(VMID_MODELE)" ]] && extra_vars+=( -e proxmox_clone_vmid_modele="$(VMID_MODELE)" ); \
|
||||||
[[ -n "$(NOEUD_PROXMOX)" ]] && extra_vars+=( -e proxmox_clone_noeud="$(NOEUD_PROXMOX)" ); \
|
[[ -n "$(NOEUD_PROXMOX)" ]] && extra_vars+=( -e proxmox_clone_noeud="$(NOEUD_PROXMOX)" ); \
|
||||||
|
|
@ -1690,15 +1542,16 @@ cloner-vm: ansible-runtime ## Clone une VM depuis le gabarit dore — HOTE=<nom>
|
||||||
;; \
|
;; \
|
||||||
esac; \
|
esac; \
|
||||||
fi; \
|
fi; \
|
||||||
ansible-playbook -i localhost, $(PLAYBOOK_PROXMOX_CLONER_VM) "$${vault_args[@]}" "$${extra_vars[@]}"
|
verrou="$${TMPDIR:-/tmp}/setops-clone-$$(printf '%s_%s' '$(NOEUD_PROXMOX)' '$(STOCKAGE_PROXMOX)' | tr -c 'A-Za-z0-9._-' '_').lock"; \
|
||||||
|
flock "$$verrou" ansible-playbook -i localhost, $(PLAYBOOK_PROXMOX_CLONER_VM) "$${vault_args[@]}" "$${extra_vars[@]}"
|
||||||
|
|
||||||
creer-vm: $(INSTANCE_OU_LOCATAIRE) ## Cree une VM et attend qu'elle soit joignable — HOTE=<nom> [TENANT=<depot>]
|
creer-vm: _instance-requise ## Cree une VM et attend qu'elle soit joignable — HOTE=<nom>
|
||||||
@set -e; \
|
@set -e; \
|
||||||
if [[ -z "$(HOTE)" ]]; then \
|
if [[ -z "$(HOTE)" ]]; then \
|
||||||
printf '%s\n' 'Refus: relancer avec HOTE=nom_hote (declare dans le plan).'; \
|
printf '%s\n' 'Refus: relancer avec HOTE=nom_hote (declare dans le plan).'; \
|
||||||
exit 2; \
|
exit 2; \
|
||||||
fi; \
|
fi; \
|
||||||
params="$$($(PARAMS_CLONAGE) $(HOTE))"; \
|
params="$$(python3 scripts/inventory_host.py --inventaire $(INVENTAIRE_PRODUCTION) parametres-proxmox --hote $(HOTE))"; \
|
||||||
eval "$$params"; \
|
eval "$$params"; \
|
||||||
export SETOPS_CLES_AMORCAGE; \
|
export SETOPS_CLES_AMORCAGE; \
|
||||||
$(MAKE) cloner-vm \
|
$(MAKE) cloner-vm \
|
||||||
|
|
@ -1739,7 +1592,7 @@ creer-vm: $(INSTANCE_OU_LOCATAIRE) ## Cree une VM et attend qu'elle soit joignab
|
||||||
@# ATTENDRE LA DISPONIBILITE est un autre geste, deplace chez `deployer` : cloud-init
|
@# 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.
|
@# et dpkg se guettent juste avant de configurer, par celui qui va configurer.
|
||||||
@if [[ "$(ATTENDRE)" != "false" ]]; then \
|
@if [[ "$(ATTENDRE)" != "false" ]]; then \
|
||||||
params="$$($(PARAMS_CLONAGE) $(HOTE))"; \
|
params="$$(python3 scripts/inventory_host.py --inventaire $(INVENTAIRE_PRODUCTION) parametres-proxmox --hote $(HOTE))"; \
|
||||||
eval "$$params"; \
|
eval "$$params"; \
|
||||||
python3 scripts/attendre_materialisation.py \
|
python3 scripts/attendre_materialisation.py \
|
||||||
--vmid "$$SETOPS_VMID" --ip "$$SETOPS_IP" --hote "$(HOTE)" \
|
--vmid "$$SETOPS_VMID" --ip "$$SETOPS_IP" --hote "$(HOTE)" \
|
||||||
|
|
|
||||||
|
|
@ -1,120 +0,0 @@
|
||||||
# Preuve de conformite — Set-OPS — 2026-09-30
|
|
||||||
|
|
||||||
> Genere par `make prouver` (`scripts/prouver.py`). **Rejouable** : relancer
|
|
||||||
> reproduit ce rapport. Chaque preuve rejoue l'outillage existant du depot ;
|
|
||||||
> aucune validation n'est reimplementee ici. Voir le mode d'emploi :
|
|
||||||
> [`docs/audit/README.md`](README.md), et le registre trace :
|
|
||||||
> [`docs/audit/affirmations.md`](affirmations.md).
|
|
||||||
|
|
||||||
- **Instance** : `/home/danallaire/Espace Chezlepro/DépôtsSurForge/Set-OPS-public/instance` — inventaire `/home/danallaire/Espace Chezlepro/DépôtsSurForge/Set-OPS-public/instance/inventories/principal/hosts.yml`
|
|
||||||
- **Verdict** : ❌ NON CONFORME (81 OK · 1 echec · 1 saute)
|
|
||||||
|
|
||||||
## Preuves
|
|
||||||
|
|
||||||
| # | Preuve | Affirmations | Statut | Detail |
|
|
||||||
|---|---|---|---|---|
|
|
||||||
| P01 | Lint (ansible-lint) | AFF-006 | ✅ OK | } \| to_nice_json }}`. |
|
|
||||||
| P02 | Tests unitaires (inventaire, raser, ecritures du plan, rendu du GUI) | — | ✅ OK | OK |
|
|
||||||
| P03 | Diff-vide du plan — TOUTES les instances | AFF-001, AFF-004, AFF-030, AFF-031, AFF-032 | ✅ OK | 4 instance(s) verifiee(s) — instance-ci-1646753, OPS-Chezlepro-lab, OPS-Chezlepro, OPS-Technolibre : plan et inventaire applique coincident. |
|
|
||||||
| P04 | Groupes <-> playbooks homonymes | AFF-008 | ✅ OK | |
|
|
||||||
| P05 | Dependances causales de groupes | AFF-009, AFF-084 | ✅ OK | |
|
|
||||||
| P06 | Validateurs de registres (serveurs/apps/bases/domaines) | AFF-003 | ✅ OK | Registre des domaines valide. |
|
|
||||||
| P07 | GUI (node --check) | AFF-033 | ✅ OK | JS du GUI : syntaxe valide (node --check), 1 nom(s) surveille(s) sans reference orpheline. |
|
|
||||||
| P08 | Orchestration (couches + graphe) | AFF-070 | ✅ OK | Orchestration coherente : 42 groupes classes, aucun cycle, aucune arete en arriere ; playbooks/site.yml a jour. |
|
|
||||||
| P09 | Flux reseau (schema + matrice) | AFF-071 | ✅ OK | Flux coherents : 40 rôles, 121 flux, schéma + matrice OK. |
|
|
||||||
| P10 | Handlers <-> notify | AFF-034, AFF-035 | ✅ OK | Tout notify pointe vers un handler du meme role (49 roles). |
|
|
||||||
| P11 | Syntaxe des playbooks (--syntax-check) | AFF-083 | ✅ OK | serveur_resolveur_site |
|
|
||||||
| P12 | Existence des runbooks cites | AFF-010, AFF-011, AFF-012, AFF-083 | ✅ OK | 17/17 runbooks/registres cites presents. |
|
|
||||||
| P13 | Invariants structurels/doctrinaux | AFF-015, AFF-022, AFF-037, AFF-038, AFF-062 | ✅ OK | LICENSE, socle dossier, pas de couches paralleles, SSH clef-only, nftables off : OK. |
|
|
||||||
| P14 | Pas de chemin lab/ code en dur | AFF-097 | ✅ OK | Aucun chemin instance/inventories/lab/group_vars code en dur. |
|
|
||||||
| P15 | Modele public socle valide | AFF-022, AFF-099 | ✅ OK | Modele public socle : domaines/serveurs/applications/bases valides. |
|
|
||||||
| P16 | Inventaire Ansible complet (--list) | AFF-030 | ⚪ SAUTE | Voute chiffree sans ANSIBLE_VAULT_PASSWORD_FILE (prerequis AFF-026). |
|
|
||||||
| P17 | Tous les modeles valident (registres + underlay) | AFF-022, AFF-099 | ✅ OK | Les 1 modele(s) decouvert(s) valident. |
|
|
||||||
| P18 | Gabarit de voute complet | AFF-026 | ✅ OK | Gabarit de voute complet : 32 secret(s) exige(s), tous presents. (Voute reelle non lisible ici : verification sautee.) |
|
|
||||||
| P19 | Le GUI couvre le schema du plan | AFF-002, AFF-095 | ✅ OK | GUI : les 30 champ(s) des plans reels sont editables (2 plan(s) inspecte(s)), registres toleres : nomenclature. |
|
|
||||||
| P20 | Adressage 100% derive du seed (aucun stocke) | AFF-001, AFF-003 | ✅ OK | 2 nomenclature(s) et 7 zone(s) de site : adressage 100% derive du seed index. |
|
|
||||||
| P21 | Federation : aucun index en collision | AFF-102 | ✅ OK | Federation coherente : 5 instance(s) federee(s), aucun index en collision. |
|
|
||||||
| P22 | Plan de recette a jour (genere du wiki) | AFF-002 | ✅ OK | Plan de recette à jour (22 sections). |
|
|
||||||
| P23 | Underlay sans collision avec la plage tenant | AFF-103 | ✅ OK | Underlay conforme : 14 reseau(x), aucune collision avec la plage tenant. |
|
|
||||||
| P24 | Frontiere nord/sud : acces d'administration declare | AFF-104 | ✅ OK | note : serveur_powerdns declare un port `derive` que le plan du site ne resout pas — aucune regle emise. |
|
|
||||||
| P25 | Pare-feu Proxmox : est-ouest intra-tenant derive | AFF-107 | ✅ OK | CONFORME : pare-feu Proxmox, 2 tenant(s), 40 groupe(s), 108 regle(s). |
|
|
||||||
| P26 | Integrations universelles : aucun hote laisse de cote | AFF-108 | ✅ OK | 13 hote(s) x 6 integration(s) universelle(s) : aucune lacune, aucune recopie (0 exemption(s) derivee(s) du service rendu). |
|
|
||||||
| P27 | Propriete des intrants : hebergeur et tenant separes | AFF-109 | ✅ OK | 8 cle(s) de cluster chez l'hebergeur, aucune recopiee dans les group_vars du tenant. |
|
|
||||||
| P28 | Pools Proxmox : un par tenant, sans collision | AFF-110 | ✅ OK | CONFORME : 3 pool(s) Proxmox, 35 VM placee(s), aucun nom ni VMID en collision. |
|
|
||||||
| P29 | Authentification : chaque role declare sa position | AFF-111 | ✅ OK | 34 role(s) serveur declares (interne-sans-auth 2, ldap-direct 2, sans-auth-humaine 23, socle-identite 2, web-sso 5) ; 2 lacune(s) nommee(s) : serveur_loki, serv |
|
|
||||||
| P30 | SDN EVPN : zones, VNets et sous-reseaux derives | AFF-112 | ✅ OK | CONFORME : SDN EVPN, 2 zone(s), 12 VNet(s), 12 sous-reseau(x), aucune collision. |
|
|
||||||
| P31 | Documentation : tout ce que le depot FAIT est nomme | — | ✅ OK | 77 scripts expliques et atteignables, 146 cibles make documentees, 69 roles avec README. |
|
|
||||||
| P32 | Intrants exiges par les roles : tous fournis | — | ✅ OK | CONFORME : 37 exigence(s) de role, toutes satisfaites (142 cle(s) declaree(s) par l'instance). |
|
|
||||||
| P33 | Aucune collision de port entre roles co-localises | — | ✅ OK | CONFORME : 44 revendication(s) de port, aucune collision entre roles co-localises (35 groupes). |
|
|
||||||
| P34 | Chaque document declare son lecteur | — | ✅ OK | 49 document(s) declarent leur lecteur (44 genere(s) exempte(s)). |
|
|
||||||
| P35 | Toute application exigeant une base en a une au plan | — | ✅ OK | 4 application(s) exigeant une base l'ont toutes (3 entree(s) au registre). |
|
|
||||||
| P36 | Tout detenteur d'etat porte une sauvegarde | — | ✅ OK | 7 hote(s) de l'ecosysteme et 3 du site detiennent de l'etat, tous porteurs de `client_backup` (8 groupe(s) au catalogue). |
|
|
||||||
| P37 | Le placement du tenant existe chez son hebergeur | — | ✅ OK | placement confronte a l'hebergeur monte (SITE-Chezlepro) : noeud, stockage, pont — tous offerts. |
|
|
||||||
| P38 | Catalogue des services : la carte dit ce que le moteur fait | — | ✅ OK | Catalogue a jour : 42 role(s) serveur/client tous nommes, 42 groupe(s) cite(s) en table existent tous. |
|
|
||||||
| P39 | Glossaire : tout mot employe est enseigne | — | ✅ OK | Glossaire complet : 81 terme(s) du jargon expliques, 15 lien(s) valides, 97 page(s) de wiki toutes atteignables. |
|
|
||||||
| P40 | Parente : l'ecosysteme sait de quoi il descend | — | ✅ OK | Parente coherente : 4 depot(s), tous retrouves, tous porteurs d'un remote. |
|
|
||||||
| P41 | Resolution d'instance : une seule, partagee | — | ✅ OK | Resolution unique : 73 script(s) passent par `inventory_rules`, 3 exemption(s) nommee(s). |
|
|
||||||
| P42 | L'edge porte les noms qu'il publie | — | ✅ OK | 4 edge(s) emettent un certificat portant les noms publies (instance-ci-1646753/production, OPS-Chezlepro-lab/principal, OPS-Chezlepro/principal, OPS-Technolibre |
|
|
||||||
| P43 | Frontiere : le devis voit les machines du site | — | ✅ OK | Devis de la frontiere : 9 machine(s) du plan retrouvees, 202 regle(s) du site. |
|
|
||||||
| P44 | Integrations : le serveur avant ses clients | — | ✅ OK | 5 integration(s) appliquent leur serveur avant leurs clients. |
|
|
||||||
| P45 | Pare-feu Proxmox : arme sur les VNet SDN, jamais ailleurs | — | ✅ OK | Le pare-feu Proxmox ne s'arme que sur un VNet SDN (4 cas evalues, dont un qui doit rendre VRAI). |
|
|
||||||
| P46 | Plancher /etc/hosts : un seul role en decide | — | ✅ OK | Un seul maitre du plancher — roles/hosts_statiques/tasks/main.yml : manage_etc_hosts: false ; et le gabarit maitre est pose (roles/hosts_statiques/templates/hos |
|
|
||||||
| P47 | Zones inverses : couvrir l'occupe, et rien de plus | — | ✅ OK | Les zones inverses couvrent l'occupe et rien de plus (5 cas evalues, dont un site a quatre zones et un tenant a une). |
|
|
||||||
| P48 | La carte d'orientation designe ce qui existe, et compte juste | — | ✅ OK | La carte designe 101 chemin(s) qui existent, et ses 7 chiffres correspondent a la mesure. |
|
|
||||||
| P49 | Registre des flux : la matrice d'audit est a jour | — | ✅ OK | Le registre des flux reproduit exactement ce que les `meta/flux.yml` declarent (139 lignes). |
|
|
||||||
| P50 | Silences : un refus muet est declare, place en dernier, et motive | — | ✅ OK | 2 silence(s) declare(s), tous en sequence > 1 (la plus haute des 309 regles `pass`), tous non consignes et tous motives. |
|
|
||||||
| P51 | Collections : toutes declarees, toutes epinglees | — | ✅ OK | 3 collection(s) et 2 bibliotheque(s) Python declarees et epinglees : ansible.posix==1.6.2, community.general==10.3.0, community.postgresql==3.10.2 |
|
|
||||||
| P52 | Materialiser n'exige pas d'entrer dans le tenant | — | ✅ OK | `creer-vm` confirme par l'agent invite (API des hyperviseurs, deja utilisee pour creer), sans exiger d'entrer dans le tenant. |
|
|
||||||
| P53 | L'interne refuse a voix haute, la bordure se tait | — | ✅ OK | L'interne parle, la bordure se tait — 13 ruleset(s) nftables refusent a voix haute ; pare-feu est-ouest en REJECT, source unique ; frontiere muette (actions : b |
|
|
||||||
| P54 | L'insemination ne reclame aucun secret du tenant | — | ✅ OK | 2 couche(s) d'insemination (serveur_debian, serveur_ops), 9 role(s) applique(s), aucun secret de tenant reclame. |
|
|
||||||
| P55 | La cle du SITE ne nait que sur le runner d'un tenant | — | ✅ OK | 13 hote(s) : la cle du SITE ne nait que sur 1 runner(s) de tenant, celle du tenant sur 13. |
|
|
||||||
| P56 | Gabarit minimal, et rien de retire n'est perdu | — | ✅ OK | Gabarit minimal : 4 role(s), tous indispensables au premier demarrage ; 14 role(s) retire(s), tous repris par le socle ou le durcissement. |
|
|
||||||
| P57 | Comptes en prose : les chiffres du depot sur lui-meme | — | ✅ OK | Les comptes ecrits en prose correspondent a la mesure (83 preuves, 69 roles, 42 groupes). |
|
|
||||||
| P58 | Habilitations : chaque service dit a quel GROUPE, et par quoi | — | ✅ OK | 8 habilitation(s) declarees, toutes nommant un groupe, un mecanisme connu et une raison ; les `role-realm` sont projetees. |
|
|
||||||
| P59 | Enumerations annoncees : le nombre correspond a ce qui suit | — | ✅ OK | 2 enumeration(s) annoncee(s) correspondent a ce qu'elles annoncent (formes non ambigues seulement). |
|
|
||||||
| P60 | Wiki publie : la forge sert ce que le depot dit | AFF-002 | ✅ OK | Le wiki publie correspond au depot : `wiki/` n'a pas bouge depuis `3fe7e3c` (publie le 2026-09-29). |
|
|
||||||
| P61 | Schema du plan : il decrit tout ce que les plans contiennent | AFF-033 | ✅ OK | Le schema decrit 56 champ(s) sur 7 registres ; il couvre tout ce que les plans reels contiennent, et la FORME de chaque champ (scalaire / objet / table) corresp |
|
|
||||||
| P62 | Schema du plan : il decrit tout ce que le MOTEUR accepte | AFF-033 | ✅ OK | Les 4 validateurs n'acceptent aucun champ que le schema ignore (applications:8, bases_donnees:4, domaines_publics:9, serveurs:3 champ(s) lus par validateur). |
|
|
||||||
| P63 | cloud-init nait avec la VM et ne lui survit pas | — | ✅ OK | cloud-init est au gabarit (la premiere seconde), absent du socle (pas de va-et-vient), et retire par le durcissement — avec la garde qui verifie que le reseau s |
|
|
||||||
| P64 | Sondes de supervision : declarees ET deposees | — | ✅ OK | 47 sonde(s) declaree(s) ET deposee(s), chacune avec sa raison et son `ttl` : client_journal/journaux, client_metrique/metriques, client_pki/certificat, client_s |
|
|
||||||
| P65 | Depots tiers : demandes au cache, jamais en HTTPS direct | — | ✅ OK | 4 depot(s) tiers relaye(s) par le cache, aucun role ne les vise en https:// ecrit en dur. |
|
|
||||||
| P66 | Clients OIDC : chaque URI vise un nom que le plan expose | — | ✅ OK | 4 client(s) OIDC, toutes leurs URI visent un FQDN que le plan expose (7 exposition(s)). |
|
|
||||||
| P67 | Nom public : le service porte celui du plan, pas celui du role | — | ✅ OK | 14 service(s) expose(s) portent le nom du plan, sur 2 inventaire(s) : instance, SITE. |
|
|
||||||
| P68 | Cle de depot telechargee : mesuree avant d'etre utilisee | — | ✅ OK | 5 role(s) telechargent une cle de depot, tous la mesurent avant de s'en servir. |
|
|
||||||
| P69 | Amorcage d'un tenant : l'adresse designe le site REEL | — | ✅ OK | 2 adresse(s) d'amorcage designent bien une machine du site. |
|
|
||||||
| P70 | Depot de binaires : il tient tout ce que les roles vont chercher | — | ✅ OK | 6 artefact(s) direct(s) tenus par le depot du site. |
|
|
||||||
| P71 | Pool du site : le genome ne nait pas chez un tenant | — | ✅ OK | `site-creer` nomme `--pool-site` ; le pool du genome ne peut plus etre celui d'un tenant. |
|
|
||||||
| P72 | Annuaire : aucun service ne se lie avec le compte du maitre | — | ✅ OK | 5 role(s) consultent l'annuaire, chacun avec SON compte de service ; seul `amorcage_acces` garde celui d'administration, et il provisionne au lieu de consommer. |
|
|
||||||
| P73 | Le locataire designe les services de son site REEL | — | ✅ OK | CONFORME : 10 intrant(s) du locataire concordent avec ce que le site expose (1 non declare(s), donc derive(s) ou non utilise(s)). |
|
|
||||||
| P74 | Gabarit dore : une seule declaration, au plan du site | — | ✅ OK | Gabarit declare une seule fois : VMID 9006 « modeleSetOPS-minimal », precedent 99998. |
|
|
||||||
| P75 | Les parametres de clonage traversent les trois maillons | — | ✅ OK | 14 parametre(s) de clonage, tous emis par l'inventaire. |
|
|
||||||
| P76 | Tout gabarit de role se rend vraiment | — | ✅ OK | 185 gabarits de role : tous se rendent. |
|
|
||||||
| P77 | Panneaux declares : assemblables, et gradues | — | ✅ OK | 8 panneau(x) declare(s) dans 2 role(s), tous avec titre, expression, raison et une unite que la table sait traduire. |
|
|
||||||
| P78 | Un consommateur de base suit le verrou TLS de son serveur | — | ✅ OK | 3 consommateur(s) suivent la posture de leur serveur ; 2 sans reglage TLS (serveur_icingaweb2, serveur_nextcloud). |
|
|
||||||
| P79 | Replis silencieux : une derivation vide ne passe pas pour un succes | — | ❌ ECHEC | 1 repli(s) silencieux — une derivation vide rend un succes :
|
|
||||||
- flux : OPS-Chezlepro-lab — perimees par rapport au plan : ops-01.nft |
|
|
||||||
| P80 | Remise au client : inscrite, nommee, et son second temps a l'heure | — | ✅ OK | Aucun ecosysteme remis a un client : rien a tenir. |
|
|
||||||
| P81 | La console dit sa portee, et ne sert pas un inventaire vide en silence | — | ✅ OK | Portee `poste` derivee des voutes portees, source d'inventaire `instance/inventories/principal/hosts.yml`, et 15 route(s) POST exigent toutes un pouvoir. |
|
|
||||||
| P82 | DNS public : les zones publiees sont servies, signees avant d'etre exposees | — | ✅ OK | 2 zone(s) publique(s) declaree(s), toutes servies par le site ; 2 replication(s) declaree(s) des deux cotes ; transfert ouvert a la cle seule ; aucune expositio |
|
|
||||||
| P83 | Assistants : le registre des runbooks ne prend pas de retard sur le Makefile | — | ✅ OK | 17 runbooks, 139 etapes, 145 cibles documentees : chacune portee par un assistant ou exemptee avec son motif. |
|
|
||||||
|
|
||||||
## Couverture des affirmations ✅ du registre
|
|
||||||
|
|
||||||
Chaque affirmation ✅ automatisable est couverte par la preuve indiquee ci-dessus.
|
|
||||||
Les ✅ **structurelles/doctrinales** non rejouables par une commande (ex. AFF-005
|
|
||||||
`make`=aide, AFF-014 ciblage groupe, AFF-024 `instancier-appliquer`, AFF-051 autorite
|
|
||||||
d'AGENTS.md, AFF-073/075 gardes `make`, AFF-090 wiki) ont ete verifiees a l'audit ;
|
|
||||||
elles restent hors du harnais recurrent (rien d'executable a rejouer).
|
|
||||||
|
|
||||||
## Declarations d'intention (⚪ invérifiables localement — assumees)
|
|
||||||
|
|
||||||
Ces affirmations ne sont pas rejouables hors production ; elles sont **assumees**
|
|
||||||
comme declarations d'intention, non comme preuves :
|
|
||||||
|
|
||||||
- **AFF-036** — « testables avec `--check` autant que possible » : verifiable seulement
|
|
||||||
contre une flotte vivante.
|
|
||||||
- **AFF-091** — contenu pedagogique du wiki : affirmations conceptuelles.
|
|
||||||
- **AFF-096** — « GUI 100 % francais » : revue exhaustive des libelles rendus, non automatisee.
|
|
||||||
- **AFF-007** — hote d'exemple `web-frontal-01` : placeholder assume.
|
|
||||||
|
|
||||||
_Rapport genere le 2026-09-30._
|
|
||||||
|
|
@ -1,119 +0,0 @@
|
||||||
# Preuve de conformite — Set-OPS — 2026-10-01
|
|
||||||
|
|
||||||
> Genere par `make prouver` (`scripts/prouver.py`). **Rejouable** : relancer
|
|
||||||
> reproduit ce rapport. Chaque preuve rejoue l'outillage existant du depot ;
|
|
||||||
> aucune validation n'est reimplementee ici. Voir le mode d'emploi :
|
|
||||||
> [`docs/audit/README.md`](README.md), et le registre trace :
|
|
||||||
> [`docs/audit/affirmations.md`](affirmations.md).
|
|
||||||
|
|
||||||
- **Instance** : `/home/danallaire/Espace Chezlepro/DépôtsSurForge/Set-OPS-public/instance` — inventaire `/home/danallaire/Espace Chezlepro/DépôtsSurForge/Set-OPS-public/instance/inventories/principal/hosts.yml`
|
|
||||||
- **Verdict** : ✅ CONFORME (82 OK · 0 echec · 1 saute)
|
|
||||||
|
|
||||||
## Preuves
|
|
||||||
|
|
||||||
| # | Preuve | Affirmations | Statut | Detail |
|
|
||||||
|---|---|---|---|---|
|
|
||||||
| P01 | Lint (ansible-lint) | AFF-006 | ✅ OK | } \| to_nice_json }}`. |
|
|
||||||
| P02 | Tests unitaires (inventaire, raser, ecritures du plan, rendu du GUI) | — | ✅ OK | OK |
|
|
||||||
| P03 | Diff-vide du plan — TOUTES les instances | AFF-001, AFF-004, AFF-030, AFF-031, AFF-032 | ✅ OK | 4 instance(s) verifiee(s) — instance-ci-1646753, OPS-Chezlepro-lab, OPS-Chezlepro, OPS-Technolibre : plan et inventaire applique coincident. |
|
|
||||||
| P04 | Groupes <-> playbooks homonymes | AFF-008 | ✅ OK | |
|
|
||||||
| P05 | Dependances causales de groupes | AFF-009, AFF-084 | ✅ OK | |
|
|
||||||
| P06 | Validateurs de registres (serveurs/apps/bases/domaines) | AFF-003 | ✅ OK | Registre des domaines valide. |
|
|
||||||
| P07 | GUI (node --check) | AFF-033 | ✅ OK | JS du GUI : syntaxe valide (node --check), 1 nom(s) surveille(s) sans reference orpheline. |
|
|
||||||
| P08 | Orchestration (couches + graphe) | AFF-070 | ✅ OK | Orchestration coherente : 42 groupes classes, aucun cycle, aucune arete en arriere ; playbooks/site.yml a jour. |
|
|
||||||
| P09 | Flux reseau (schema + matrice) | AFF-071 | ✅ OK | Flux coherents : 40 rôles, 121 flux, schéma + matrice OK. |
|
|
||||||
| P10 | Handlers <-> notify | AFF-034, AFF-035 | ✅ OK | Tout notify pointe vers un handler du meme role (49 roles). |
|
|
||||||
| P11 | Syntaxe des playbooks (--syntax-check) | AFF-083 | ✅ OK | serveur_resolveur_site |
|
|
||||||
| P12 | Existence des runbooks cites | AFF-010, AFF-011, AFF-012, AFF-083 | ✅ OK | 17/17 runbooks/registres cites presents. |
|
|
||||||
| P13 | Invariants structurels/doctrinaux | AFF-015, AFF-022, AFF-037, AFF-038, AFF-062 | ✅ OK | LICENSE, socle dossier, pas de couches paralleles, SSH clef-only, nftables off : OK. |
|
|
||||||
| P14 | Pas de chemin lab/ code en dur | AFF-097 | ✅ OK | Aucun chemin instance/inventories/lab/group_vars code en dur. |
|
|
||||||
| P15 | Modele public socle valide | AFF-022, AFF-099 | ✅ OK | Modele public socle : domaines/serveurs/applications/bases valides. |
|
|
||||||
| P16 | Inventaire Ansible complet (--list) | AFF-030 | ⚪ SAUTE | Voute chiffree sans ANSIBLE_VAULT_PASSWORD_FILE (prerequis AFF-026). |
|
|
||||||
| P17 | Tous les modeles valident (registres + underlay) | AFF-022, AFF-099 | ✅ OK | Les 1 modele(s) decouvert(s) valident. |
|
|
||||||
| P18 | Gabarit de voute complet | AFF-026 | ✅ OK | Gabarit de voute complet : 32 secret(s) exige(s), tous presents. (Voute reelle non lisible ici : verification sautee.) |
|
|
||||||
| P19 | Le GUI couvre le schema du plan | AFF-002, AFF-095 | ✅ OK | GUI : les 30 champ(s) des plans reels sont editables (2 plan(s) inspecte(s)), registres toleres : nomenclature. |
|
|
||||||
| P20 | Adressage 100% derive du seed (aucun stocke) | AFF-001, AFF-003 | ✅ OK | 2 nomenclature(s) et 7 zone(s) de site : adressage 100% derive du seed index. |
|
|
||||||
| P21 | Federation : aucun index en collision | AFF-102 | ✅ OK | Federation coherente : 5 instance(s) federee(s), aucun index en collision. |
|
|
||||||
| P22 | Plan de recette a jour (genere du wiki) | AFF-002 | ✅ OK | Plan de recette à jour (22 sections). |
|
|
||||||
| P23 | Underlay sans collision avec la plage tenant | AFF-103 | ✅ OK | Underlay conforme : 14 reseau(x), aucune collision avec la plage tenant. |
|
|
||||||
| P24 | Frontiere nord/sud : acces d'administration declare | AFF-104 | ✅ OK | note : serveur_powerdns declare un port `derive` que le plan du site ne resout pas — aucune regle emise. |
|
|
||||||
| P25 | Pare-feu Proxmox : est-ouest intra-tenant derive | AFF-107 | ✅ OK | CONFORME : pare-feu Proxmox, 2 tenant(s), 40 groupe(s), 108 regle(s). |
|
|
||||||
| P26 | Integrations universelles : aucun hote laisse de cote | AFF-108 | ✅ OK | 13 hote(s) x 6 integration(s) universelle(s) : aucune lacune, aucune recopie (0 exemption(s) derivee(s) du service rendu). |
|
|
||||||
| P27 | Propriete des intrants : hebergeur et tenant separes | AFF-109 | ✅ OK | 8 cle(s) de cluster chez l'hebergeur, aucune recopiee dans les group_vars du tenant. |
|
|
||||||
| P28 | Pools Proxmox : un par tenant, sans collision | AFF-110 | ✅ OK | CONFORME : 3 pool(s) Proxmox, 35 VM placee(s), aucun nom ni VMID en collision. |
|
|
||||||
| P29 | Authentification : chaque role declare sa position | AFF-111 | ✅ OK | 34 role(s) serveur declares (interne-sans-auth 2, ldap-direct 2, sans-auth-humaine 23, socle-identite 2, web-sso 5) ; 2 lacune(s) nommee(s) : serveur_loki, serv |
|
|
||||||
| P30 | SDN EVPN : zones, VNets et sous-reseaux derives | AFF-112 | ✅ OK | CONFORME : SDN EVPN, 2 zone(s), 12 VNet(s), 12 sous-reseau(x), aucune collision. |
|
|
||||||
| P31 | Documentation : tout ce que le depot FAIT est nomme | — | ✅ OK | 77 scripts expliques et atteignables, 146 cibles make documentees, 69 roles avec README. |
|
|
||||||
| P32 | Intrants exiges par les roles : tous fournis | — | ✅ OK | CONFORME : 37 exigence(s) de role, toutes satisfaites (142 cle(s) declaree(s) par l'instance). |
|
|
||||||
| P33 | Aucune collision de port entre roles co-localises | — | ✅ OK | CONFORME : 44 revendication(s) de port, aucune collision entre roles co-localises (35 groupes). |
|
|
||||||
| P34 | Chaque document declare son lecteur | — | ✅ OK | 49 document(s) declarent leur lecteur (45 genere(s) exempte(s)). |
|
|
||||||
| P35 | Toute application exigeant une base en a une au plan | — | ✅ OK | 4 application(s) exigeant une base l'ont toutes (3 entree(s) au registre). |
|
|
||||||
| P36 | Tout detenteur d'etat porte une sauvegarde | — | ✅ OK | 7 hote(s) de l'ecosysteme et 3 du site detiennent de l'etat, tous porteurs de `client_backup` (8 groupe(s) au catalogue). |
|
|
||||||
| P37 | Le placement du tenant existe chez son hebergeur | — | ✅ OK | placement confronte a l'hebergeur monte (SITE-Chezlepro) : noeud, stockage, pont — tous offerts. |
|
|
||||||
| P38 | Catalogue des services : la carte dit ce que le moteur fait | — | ✅ OK | Catalogue a jour : 42 role(s) serveur/client tous nommes, 42 groupe(s) cite(s) en table existent tous. |
|
|
||||||
| P39 | Glossaire : tout mot employe est enseigne | — | ✅ OK | Glossaire complet : 81 terme(s) du jargon expliques, 15 lien(s) valides, 97 page(s) de wiki toutes atteignables. |
|
|
||||||
| P40 | Parente : l'ecosysteme sait de quoi il descend | — | ✅ OK | Parente coherente : 4 depot(s), tous retrouves, tous porteurs d'un remote. |
|
|
||||||
| P41 | Resolution d'instance : une seule, partagee | — | ✅ OK | Resolution unique : 73 script(s) passent par `inventory_rules`, 3 exemption(s) nommee(s). |
|
|
||||||
| P42 | L'edge porte les noms qu'il publie | — | ✅ OK | 4 edge(s) emettent un certificat portant les noms publies (instance-ci-1646753/production, OPS-Chezlepro-lab/principal, OPS-Chezlepro/principal, OPS-Technolibre |
|
|
||||||
| P43 | Frontiere : le devis voit les machines du site | — | ✅ OK | Devis de la frontiere : 9 machine(s) du plan retrouvees, 202 regle(s) du site. |
|
|
||||||
| P44 | Integrations : le serveur avant ses clients | — | ✅ OK | 5 integration(s) appliquent leur serveur avant leurs clients. |
|
|
||||||
| P45 | Pare-feu Proxmox : arme sur les VNet SDN, jamais ailleurs | — | ✅ OK | Le pare-feu Proxmox ne s'arme que sur un VNet SDN (4 cas evalues, dont un qui doit rendre VRAI). |
|
|
||||||
| P46 | Plancher /etc/hosts : un seul role en decide | — | ✅ OK | Un seul maitre du plancher — roles/hosts_statiques/tasks/main.yml : manage_etc_hosts: false ; et le gabarit maitre est pose (roles/hosts_statiques/templates/hos |
|
|
||||||
| P47 | Zones inverses : couvrir l'occupe, et rien de plus | — | ✅ OK | Les zones inverses couvrent l'occupe et rien de plus (5 cas evalues, dont un site a quatre zones et un tenant a une). |
|
|
||||||
| P48 | La carte d'orientation designe ce qui existe, et compte juste | — | ✅ OK | La carte designe 101 chemin(s) qui existent, et ses 7 chiffres correspondent a la mesure. |
|
|
||||||
| P49 | Registre des flux : la matrice d'audit est a jour | — | ✅ OK | Le registre des flux reproduit exactement ce que les `meta/flux.yml` declarent (139 lignes). |
|
|
||||||
| P50 | Silences : un refus muet est declare, place en dernier, et motive | — | ✅ OK | 2 silence(s) declare(s), tous en sequence > 1 (la plus haute des 309 regles `pass`), tous non consignes et tous motives. |
|
|
||||||
| P51 | Collections : toutes declarees, toutes epinglees | — | ✅ OK | 3 collection(s) et 2 bibliotheque(s) Python declarees et epinglees : ansible.posix==1.6.2, community.general==10.3.0, community.postgresql==3.10.2 |
|
|
||||||
| P52 | Materialiser n'exige pas d'entrer dans le tenant | — | ✅ OK | `creer-vm` confirme par l'agent invite (API des hyperviseurs, deja utilisee pour creer), sans exiger d'entrer dans le tenant. |
|
|
||||||
| P53 | L'interne refuse a voix haute, la bordure se tait | — | ✅ OK | L'interne parle, la bordure se tait — 13 ruleset(s) nftables refusent a voix haute ; pare-feu est-ouest en REJECT, source unique ; frontiere : WAN muet, interfa |
|
|
||||||
| P54 | L'insemination ne reclame aucun secret du tenant | — | ✅ OK | 2 couche(s) d'insemination (serveur_debian, serveur_ops), 9 role(s) applique(s), aucun secret de tenant reclame. |
|
|
||||||
| P55 | La cle du SITE ne nait que sur le runner d'un tenant | — | ✅ OK | 13 hote(s) : la cle du SITE ne nait que sur 1 runner(s) de tenant, celle du tenant sur 13. |
|
|
||||||
| P56 | Gabarit minimal, et rien de retire n'est perdu | — | ✅ OK | Gabarit minimal : 4 role(s), tous indispensables au premier demarrage ; 14 role(s) retire(s), tous repris par le socle ou le durcissement. |
|
|
||||||
| P57 | Comptes en prose : les chiffres du depot sur lui-meme | — | ✅ OK | Les comptes ecrits en prose correspondent a la mesure (83 preuves, 69 roles, 42 groupes). |
|
|
||||||
| P58 | Habilitations : chaque service dit a quel GROUPE, et par quoi | — | ✅ OK | 8 habilitation(s) declarees, toutes nommant un groupe, un mecanisme connu et une raison ; les `role-realm` sont projetees. |
|
|
||||||
| P59 | Enumerations annoncees : le nombre correspond a ce qui suit | — | ✅ OK | 2 enumeration(s) annoncee(s) correspondent a ce qu'elles annoncent (formes non ambigues seulement). |
|
|
||||||
| P60 | Wiki publie : la forge sert ce que le depot dit | AFF-002 | ✅ OK | Le wiki publie correspond au depot : `wiki/` n'a pas bouge depuis `3fe7e3c` (publie le 2026-09-29). |
|
|
||||||
| P61 | Schema du plan : il decrit tout ce que les plans contiennent | AFF-033 | ✅ OK | Le schema decrit 56 champ(s) sur 7 registres ; il couvre tout ce que les plans reels contiennent, et la FORME de chaque champ (scalaire / objet / table) corresp |
|
|
||||||
| P62 | Schema du plan : il decrit tout ce que le MOTEUR accepte | AFF-033 | ✅ OK | Les 4 validateurs n'acceptent aucun champ que le schema ignore (applications:8, bases_donnees:4, domaines_publics:9, serveurs:3 champ(s) lus par validateur). |
|
|
||||||
| P63 | cloud-init nait avec la VM et ne lui survit pas | — | ✅ OK | cloud-init est au gabarit (la premiere seconde), absent du socle (pas de va-et-vient), et retire par le durcissement — avec la garde qui verifie que le reseau s |
|
|
||||||
| P64 | Sondes de supervision : declarees ET deposees | — | ✅ OK | 47 sonde(s) declaree(s) ET deposee(s), chacune avec sa raison et son `ttl` : client_journal/journaux, client_metrique/metriques, client_pki/certificat, client_s |
|
|
||||||
| P65 | Depots tiers : demandes au cache, jamais en HTTPS direct | — | ✅ OK | 4 depot(s) tiers relaye(s) par le cache, aucun role ne les vise en https:// ecrit en dur. |
|
|
||||||
| P66 | Clients OIDC : chaque URI vise un nom que le plan expose | — | ✅ OK | 4 client(s) OIDC, toutes leurs URI visent un FQDN que le plan expose (7 exposition(s)). |
|
|
||||||
| P67 | Nom public : le service porte celui du plan, pas celui du role | — | ✅ OK | 14 service(s) expose(s) portent le nom du plan, sur 2 inventaire(s) : instance, SITE. |
|
|
||||||
| P68 | Cle de depot telechargee : mesuree avant d'etre utilisee | — | ✅ OK | 5 role(s) telechargent une cle de depot, tous la mesurent avant de s'en servir. |
|
|
||||||
| P69 | Amorcage d'un tenant : l'adresse designe le site REEL | — | ✅ OK | 2 adresse(s) d'amorcage designent bien une machine du site. |
|
|
||||||
| P70 | Depot de binaires : il tient tout ce que les roles vont chercher | — | ✅ OK | 6 artefact(s) direct(s) tenus par le depot du site. |
|
|
||||||
| P71 | Pool du site : le genome ne nait pas chez un tenant | — | ✅ OK | `site-creer` nomme `--pool-site` ; le pool du genome ne peut plus etre celui d'un tenant. |
|
|
||||||
| P72 | Annuaire : aucun service ne se lie avec le compte du maitre | — | ✅ OK | 5 role(s) consultent l'annuaire, chacun avec SON compte de service ; seul `amorcage_acces` garde celui d'administration, et il provisionne au lieu de consommer. |
|
|
||||||
| P73 | Le locataire designe les services de son site REEL | — | ✅ OK | CONFORME : 10 intrant(s) du locataire concordent avec ce que le site expose (1 non declare(s), donc derive(s) ou non utilise(s)). |
|
|
||||||
| P74 | Gabarit dore : une seule declaration, au plan du site | — | ✅ OK | Gabarit declare une seule fois : VMID 9006 « modeleSetOPS-minimal », precedent 99998. |
|
|
||||||
| P75 | Les parametres de clonage traversent les trois maillons | — | ✅ OK | 14 parametre(s) de clonage, tous emis par l'inventaire. |
|
|
||||||
| P76 | Tout gabarit de role se rend vraiment | — | ✅ OK | 185 gabarits de role : tous se rendent. |
|
|
||||||
| P77 | Panneaux declares : assemblables, et gradues | — | ✅ OK | 8 panneau(x) declare(s) dans 2 role(s), tous avec titre, expression, raison et une unite que la table sait traduire. |
|
|
||||||
| P78 | Un consommateur de base suit le verrou TLS de son serveur | — | ✅ OK | 3 consommateur(s) suivent la posture de leur serveur ; 2 sans reglage TLS (serveur_icingaweb2, serveur_nextcloud). |
|
|
||||||
| P79 | Replis silencieux : une derivation vide ne passe pas pour un succes | — | ✅ OK | 5 ecosysteme(s) (instance-ci-1646753, OPS-Chezlepro-lab, OPS-Chezlepro, OPS-Technolibre, SITE-Chezlepro) : pattes, edges, certificats, rechargements, jumeaux d' |
|
|
||||||
| P80 | Remise au client : inscrite, nommee, et son second temps a l'heure | — | ✅ OK | Aucun ecosysteme remis a un client : rien a tenir. |
|
|
||||||
| P81 | La console dit sa portee, et ne sert pas un inventaire vide en silence | — | ✅ OK | Portee `poste` derivee des voutes portees, source d'inventaire `instance/inventories/principal/hosts.yml`, et 15 route(s) POST exigent toutes un pouvoir. |
|
|
||||||
| P82 | DNS public : les zones publiees sont servies, signees avant d'etre exposees | — | ✅ OK | 2 zone(s) publique(s) declaree(s), toutes servies par le site ; 2 replication(s) declaree(s) des deux cotes ; transfert ouvert a la cle seule ; aucune expositio |
|
|
||||||
| P83 | Assistants : le registre des runbooks ne prend pas de retard sur le Makefile | — | ✅ OK | 17 runbooks, 139 etapes, 145 cibles documentees : chacune portee par un assistant ou exemptee avec son motif. |
|
|
||||||
|
|
||||||
## Couverture des affirmations ✅ du registre
|
|
||||||
|
|
||||||
Chaque affirmation ✅ automatisable est couverte par la preuve indiquee ci-dessus.
|
|
||||||
Les ✅ **structurelles/doctrinales** non rejouables par une commande (ex. AFF-005
|
|
||||||
`make`=aide, AFF-014 ciblage groupe, AFF-024 `instancier-appliquer`, AFF-051 autorite
|
|
||||||
d'AGENTS.md, AFF-073/075 gardes `make`, AFF-090 wiki) ont ete verifiees a l'audit ;
|
|
||||||
elles restent hors du harnais recurrent (rien d'executable a rejouer).
|
|
||||||
|
|
||||||
## Declarations d'intention (⚪ invérifiables localement — assumees)
|
|
||||||
|
|
||||||
Ces affirmations ne sont pas rejouables hors production ; elles sont **assumees**
|
|
||||||
comme declarations d'intention, non comme preuves :
|
|
||||||
|
|
||||||
- **AFF-036** — « testables avec `--check` autant que possible » : verifiable seulement
|
|
||||||
contre une flotte vivante.
|
|
||||||
- **AFF-091** — contenu pedagogique du wiki : affirmations conceptuelles.
|
|
||||||
- **AFF-096** — « GUI 100 % francais » : revue exhaustive des libelles rendus, non automatisee.
|
|
||||||
- **AFF-007** — hote d'exemple `web-frontal-01` : placeholder assume.
|
|
||||||
|
|
||||||
_Rapport genere le 2026-10-01._
|
|
||||||
|
|
@ -1,120 +0,0 @@
|
||||||
# Preuve de conformite — Set-OPS — 2026-10-03
|
|
||||||
|
|
||||||
> Genere par `make prouver` (`scripts/prouver.py`). **Rejouable** : relancer
|
|
||||||
> reproduit ce rapport. Chaque preuve rejoue l'outillage existant du depot ;
|
|
||||||
> aucune validation n'est reimplementee ici. Voir le mode d'emploi :
|
|
||||||
> [`docs/audit/README.md`](README.md), et le registre trace :
|
|
||||||
> [`docs/audit/affirmations.md`](affirmations.md).
|
|
||||||
|
|
||||||
- **Instance** : `/home/danallaire/Espace Chezlepro/DépôtsSurForge/Set-OPS-public/instance` — inventaire `/home/danallaire/Espace Chezlepro/DépôtsSurForge/Set-OPS-public/instance/inventories/principal/hosts.yml`
|
|
||||||
- **Verdict** : ❌ NON CONFORME (81 OK · 1 echec · 1 saute)
|
|
||||||
|
|
||||||
## Preuves
|
|
||||||
|
|
||||||
| # | Preuve | Affirmations | Statut | Detail |
|
|
||||||
|---|---|---|---|---|
|
|
||||||
| P01 | Lint (ansible-lint) | AFF-006 | ✅ OK | [0m[0m |
|
|
||||||
| P02 | Tests unitaires (inventaire, raser, ecritures du plan, rendu du GUI) | — | ✅ OK | OK |
|
|
||||||
| P03 | Diff-vide du plan — TOUTES les instances | AFF-001, AFF-004, AFF-030, AFF-031, AFF-032 | ✅ OK | 4 instance(s) verifiee(s) — instance-ci-1646753, OPS-Chezlepro-lab, OPS-Chezlepro, OPS-Technolibre : plan et inventaire applique coincident. |
|
|
||||||
| P04 | Groupes <-> playbooks homonymes | AFF-008 | ✅ OK | |
|
|
||||||
| P05 | Dependances causales de groupes | AFF-009, AFF-084 | ✅ OK | |
|
|
||||||
| P06 | Validateurs de registres (serveurs/apps/bases/domaines) | AFF-003 | ✅ OK | Registre des domaines valide. |
|
|
||||||
| P07 | GUI (node --check) | AFF-033 | ✅ OK | JS du GUI : syntaxe valide (node --check), 1 nom(s) surveille(s) sans reference orpheline. |
|
|
||||||
| P08 | Orchestration (couches + graphe) | AFF-070 | ✅ OK | Orchestration coherente : 42 groupes classes, aucun cycle, aucune arete en arriere ; playbooks/site.yml a jour. |
|
|
||||||
| P09 | Flux reseau (schema + matrice) | AFF-071 | ✅ OK | Flux coherents : 40 rôles, 121 flux, schéma + matrice OK. |
|
|
||||||
| P10 | Handlers <-> notify | AFF-034, AFF-035 | ✅ OK | Tout notify pointe vers un handler du meme role (49 roles). |
|
|
||||||
| P11 | Syntaxe des playbooks (--syntax-check) | AFF-083 | ✅ OK | serveur_resolveur_site |
|
|
||||||
| P12 | Existence des runbooks cites | AFF-010, AFF-011, AFF-012, AFF-083 | ✅ OK | 17/17 runbooks/registres cites presents. |
|
|
||||||
| P13 | Invariants structurels/doctrinaux | AFF-015, AFF-022, AFF-037, AFF-038, AFF-062 | ✅ OK | LICENSE, socle dossier, pas de couches paralleles, SSH clef-only, nftables off : OK. |
|
|
||||||
| P14 | Pas de chemin lab/ code en dur | AFF-097 | ✅ OK | Aucun chemin instance/inventories/lab/group_vars code en dur. |
|
|
||||||
| P15 | Modele public socle valide | AFF-022, AFF-099 | ✅ OK | Modele public socle : domaines/serveurs/applications/bases valides. |
|
|
||||||
| P16 | Inventaire Ansible complet (--list) | AFF-030 | ⚪ SAUTE | Voute chiffree sans ANSIBLE_VAULT_PASSWORD_FILE (prerequis AFF-026). |
|
|
||||||
| P17 | Tous les modeles valident (registres + underlay) | AFF-022, AFF-099 | ✅ OK | Les 1 modele(s) decouvert(s) valident. |
|
|
||||||
| P18 | Gabarit de voute complet | AFF-026 | ✅ OK | Gabarit de voute complet : 32 secret(s) exige(s), tous presents. (Voute reelle non lisible ici : verification sautee.) |
|
|
||||||
| P19 | Le GUI couvre le schema du plan | AFF-002, AFF-095 | ✅ OK | GUI : les 30 champ(s) des plans reels sont editables (2 plan(s) inspecte(s)), registres toleres : nomenclature. |
|
|
||||||
| P20 | Adressage 100% derive du seed (aucun stocke) | AFF-001, AFF-003 | ✅ OK | 2 nomenclature(s) et 7 zone(s) de site : adressage 100% derive du seed index. |
|
|
||||||
| P21 | Federation : aucun index en collision | AFF-102 | ✅ OK | Federation coherente : 5 instance(s) federee(s), aucun index en collision. |
|
|
||||||
| P22 | Plan de recette a jour (genere du wiki) | AFF-002 | ✅ OK | Plan de recette à jour (22 sections). |
|
|
||||||
| P23 | Underlay sans collision avec la plage tenant | AFF-103 | ✅ OK | Underlay conforme : 14 reseau(x), aucune collision avec la plage tenant. |
|
|
||||||
| P24 | Frontiere nord/sud : acces d'administration declare | AFF-104 | ✅ OK | note : serveur_powerdns declare un port `derive` que le plan du site ne resout pas — aucune regle emise. |
|
|
||||||
| P25 | Pare-feu Proxmox : est-ouest intra-tenant derive | AFF-107 | ✅ OK | CONFORME : pare-feu Proxmox, 2 tenant(s), 40 groupe(s), 108 regle(s). |
|
|
||||||
| P26 | Integrations universelles : aucun hote laisse de cote | AFF-108 | ✅ OK | 13 hote(s) x 6 integration(s) universelle(s) : aucune lacune, aucune recopie (0 exemption(s) derivee(s) du service rendu). |
|
|
||||||
| P27 | Propriete des intrants : hebergeur et tenant separes | AFF-109 | ✅ OK | 8 cle(s) de cluster chez l'hebergeur, aucune recopiee dans les group_vars du tenant. |
|
|
||||||
| P28 | Pools Proxmox : un par tenant, sans collision | AFF-110 | ✅ OK | CONFORME : 3 pool(s) Proxmox, 35 VM placee(s), aucun nom ni VMID en collision. |
|
|
||||||
| P29 | Authentification : chaque role declare sa position | AFF-111 | ✅ OK | 34 role(s) serveur declares (interne-sans-auth 2, ldap-direct 2, sans-auth-humaine 23, socle-identite 2, web-sso 5) ; 2 lacune(s) nommee(s) : serveur_loki, serv |
|
|
||||||
| P30 | SDN EVPN : zones, VNets et sous-reseaux derives | AFF-112 | ✅ OK | CONFORME : SDN EVPN, 2 zone(s), 12 VNet(s), 12 sous-reseau(x), aucune collision. |
|
|
||||||
| P31 | Documentation : tout ce que le depot FAIT est nomme | — | ✅ OK | 77 scripts expliques et atteignables, 146 cibles make documentees, 69 roles avec README. |
|
|
||||||
| P32 | Intrants exiges par les roles : tous fournis | — | ✅ OK | CONFORME : 37 exigence(s) de role, toutes satisfaites (142 cle(s) declaree(s) par l'instance). |
|
|
||||||
| P33 | Aucune collision de port entre roles co-localises | — | ✅ OK | CONFORME : 44 revendication(s) de port, aucune collision entre roles co-localises (35 groupes). |
|
|
||||||
| P34 | Chaque document declare son lecteur | — | ✅ OK | 49 document(s) declarent leur lecteur (46 genere(s) exempte(s)). |
|
|
||||||
| P35 | Toute application exigeant une base en a une au plan | — | ✅ OK | 4 application(s) exigeant une base l'ont toutes (3 entree(s) au registre). |
|
|
||||||
| P36 | Tout detenteur d'etat porte une sauvegarde | — | ✅ OK | 7 hote(s) de l'ecosysteme et 3 du site detiennent de l'etat, tous porteurs de `client_backup` (8 groupe(s) au catalogue). |
|
|
||||||
| P37 | Le placement du tenant existe chez son hebergeur | — | ✅ OK | placement confronte a l'hebergeur monte (SITE-Chezlepro) : noeud, stockage, pont — tous offerts. |
|
|
||||||
| P38 | Catalogue des services : la carte dit ce que le moteur fait | — | ✅ OK | Catalogue a jour : 42 role(s) serveur/client tous nommes, 42 groupe(s) cite(s) en table existent tous. |
|
|
||||||
| P39 | Glossaire : tout mot employe est enseigne | — | ✅ OK | Glossaire complet : 81 terme(s) du jargon expliques, 15 lien(s) valides, 97 page(s) de wiki toutes atteignables. |
|
|
||||||
| P40 | Parente : l'ecosysteme sait de quoi il descend | — | ✅ OK | Parente coherente : 4 depot(s), tous retrouves, tous porteurs d'un remote. |
|
|
||||||
| P41 | Resolution d'instance : une seule, partagee | — | ✅ OK | Resolution unique : 73 script(s) passent par `inventory_rules`, 3 exemption(s) nommee(s). |
|
|
||||||
| P42 | L'edge porte les noms qu'il publie | — | ✅ OK | 4 edge(s) emettent un certificat portant les noms publies (instance-ci-1646753/production, OPS-Chezlepro-lab/principal, OPS-Chezlepro/principal, OPS-Technolibre |
|
|
||||||
| P43 | Frontiere : le devis voit les machines du site | — | ✅ OK | Devis de la frontiere : 9 machine(s) du plan retrouvees, 202 regle(s) du site. |
|
|
||||||
| P44 | Integrations : le serveur avant ses clients | — | ✅ OK | 5 integration(s) appliquent leur serveur avant leurs clients. |
|
|
||||||
| P45 | Pare-feu Proxmox : arme sur les VNet SDN, jamais ailleurs | — | ✅ OK | Le pare-feu Proxmox ne s'arme que sur un VNet SDN (4 cas evalues, dont un qui doit rendre VRAI). |
|
|
||||||
| P46 | Plancher /etc/hosts : un seul role en decide | — | ✅ OK | Un seul maitre du plancher — roles/hosts_statiques/tasks/main.yml : manage_etc_hosts: false ; et le gabarit maitre est pose (roles/hosts_statiques/templates/hos |
|
|
||||||
| P47 | Zones inverses : couvrir l'occupe, et rien de plus | — | ✅ OK | Les zones inverses couvrent l'occupe et rien de plus (5 cas evalues, dont un site a quatre zones et un tenant a une). |
|
|
||||||
| P48 | La carte d'orientation designe ce qui existe, et compte juste | — | ❌ ECHEC | La carte d'orientation ne dit plus vrai :
|
|
||||||
- « pieces d'audit » : la carte annonce 50, le depot en compte 51 |
|
|
||||||
| P49 | Registre des flux : la matrice d'audit est a jour | — | ✅ OK | Le registre des flux reproduit exactement ce que les `meta/flux.yml` declarent (139 lignes). |
|
|
||||||
| P50 | Silences : un refus muet est declare, place en dernier, et motive | — | ✅ OK | 2 silence(s) declare(s), tous en sequence > 1 (la plus haute des 309 regles `pass`), tous non consignes et tous motives. |
|
|
||||||
| P51 | Collections : toutes declarees, toutes epinglees | — | ✅ OK | 3 collection(s) et 2 bibliotheque(s) Python declarees et epinglees : ansible.posix==1.6.2, community.general==10.3.0, community.postgresql==3.10.2 |
|
|
||||||
| P52 | Materialiser n'exige pas d'entrer dans le tenant | — | ✅ OK | `creer-vm` confirme par l'agent invite (API des hyperviseurs, deja utilisee pour creer), sans exiger d'entrer dans le tenant. |
|
|
||||||
| P53 | L'interne refuse a voix haute, la bordure se tait | — | ✅ OK | L'interne parle, la bordure se tait — 13 ruleset(s) nftables refusent a voix haute ; pare-feu est-ouest en REJECT, source unique ; frontiere : WAN muet, interfa |
|
|
||||||
| P54 | L'insemination ne reclame aucun secret du tenant | — | ✅ OK | 2 couche(s) d'insemination (serveur_debian, serveur_ops), 9 role(s) applique(s), aucun secret de tenant reclame. |
|
|
||||||
| P55 | La cle du SITE ne nait que sur le runner d'un tenant | — | ✅ OK | 13 hote(s) : la cle du SITE ne nait que sur 1 runner(s) de tenant, celle du tenant sur 13. |
|
|
||||||
| P56 | Gabarit minimal, et rien de retire n'est perdu | — | ✅ OK | Gabarit minimal : 4 role(s), tous indispensables au premier demarrage ; 14 role(s) retire(s), tous repris par le socle ou le durcissement. |
|
|
||||||
| P57 | Comptes en prose : les chiffres du depot sur lui-meme | — | ✅ OK | Les comptes ecrits en prose correspondent a la mesure (83 preuves, 69 roles, 42 groupes). |
|
|
||||||
| P58 | Habilitations : chaque service dit a quel GROUPE, et par quoi | — | ✅ OK | 8 habilitation(s) declarees, toutes nommant un groupe, un mecanisme connu et une raison ; les `role-realm` sont projetees. |
|
|
||||||
| P59 | Enumerations annoncees : le nombre correspond a ce qui suit | — | ✅ OK | 2 enumeration(s) annoncee(s) correspondent a ce qu'elles annoncent (formes non ambigues seulement). |
|
|
||||||
| P60 | Wiki publie : la forge sert ce que le depot dit | AFF-002 | ✅ OK | Le wiki publie correspond au depot : `wiki/` n'a pas bouge depuis `3fe7e3c` (publie le 2026-09-29). |
|
|
||||||
| P61 | Schema du plan : il decrit tout ce que les plans contiennent | AFF-033 | ✅ OK | Le schema decrit 56 champ(s) sur 7 registres ; il couvre tout ce que les plans reels contiennent, et la FORME de chaque champ (scalaire / objet / table) corresp |
|
|
||||||
| P62 | Schema du plan : il decrit tout ce que le MOTEUR accepte | AFF-033 | ✅ OK | Les 4 validateurs n'acceptent aucun champ que le schema ignore (applications:8, bases_donnees:4, domaines_publics:9, serveurs:3 champ(s) lus par validateur). |
|
|
||||||
| P63 | cloud-init nait avec la VM et ne lui survit pas | — | ✅ OK | cloud-init est au gabarit (la premiere seconde), absent du socle (pas de va-et-vient), et retire par le durcissement — avec la garde qui verifie que le reseau s |
|
|
||||||
| P64 | Sondes de supervision : declarees ET deposees | — | ✅ OK | 47 sonde(s) declaree(s) ET deposee(s), chacune avec sa raison et son `ttl` : client_journal/journaux, client_metrique/metriques, client_pki/certificat, client_s |
|
|
||||||
| P65 | Depots tiers : demandes au cache, jamais en HTTPS direct | — | ✅ OK | 4 depot(s) tiers relaye(s) par le cache, aucun role ne les vise en https:// ecrit en dur. |
|
|
||||||
| P66 | Clients OIDC : chaque URI vise un nom que le plan expose | — | ✅ OK | 4 client(s) OIDC, toutes leurs URI visent un FQDN que le plan expose (7 exposition(s)). |
|
|
||||||
| P67 | Nom public : le service porte celui du plan, pas celui du role | — | ✅ OK | 14 service(s) expose(s) portent le nom du plan, sur 2 inventaire(s) : instance, SITE. |
|
|
||||||
| P68 | Cle de depot telechargee : mesuree avant d'etre utilisee | — | ✅ OK | 5 role(s) telechargent une cle de depot, tous la mesurent avant de s'en servir. |
|
|
||||||
| P69 | Amorcage d'un tenant : l'adresse designe le site REEL | — | ✅ OK | 2 adresse(s) d'amorcage designent bien une machine du site. |
|
|
||||||
| P70 | Depot de binaires : il tient tout ce que les roles vont chercher | — | ✅ OK | 6 artefact(s) direct(s) tenus par le depot du site. |
|
|
||||||
| P71 | Pool du site : le genome ne nait pas chez un tenant | — | ✅ OK | `site-creer` nomme `--pool-site` ; le pool du genome ne peut plus etre celui d'un tenant. |
|
|
||||||
| P72 | Annuaire : aucun service ne se lie avec le compte du maitre | — | ✅ OK | 5 role(s) consultent l'annuaire, chacun avec SON compte de service ; seul `amorcage_acces` garde celui d'administration, et il provisionne au lieu de consommer. |
|
|
||||||
| P73 | Le locataire designe les services de son site REEL | — | ✅ OK | CONFORME : 10 intrant(s) du locataire concordent avec ce que le site expose (1 non declare(s), donc derive(s) ou non utilise(s)). |
|
|
||||||
| P74 | Gabarit dore : une seule declaration, au plan du site | — | ✅ OK | Gabarit declare une seule fois : VMID 9006 « modeleSetOPS-minimal », precedent 99998. |
|
|
||||||
| P75 | Les parametres de clonage traversent les trois maillons | — | ✅ OK | 14 parametre(s) de clonage, tous emis par l'inventaire. |
|
|
||||||
| P76 | Tout gabarit de role se rend vraiment | — | ✅ OK | 185 gabarits de role : tous se rendent. |
|
|
||||||
| P77 | Panneaux declares : assemblables, et gradues | — | ✅ OK | 8 panneau(x) declare(s) dans 2 role(s), tous avec titre, expression, raison et une unite que la table sait traduire. |
|
|
||||||
| P78 | Un consommateur de base suit le verrou TLS de son serveur | — | ✅ OK | 3 consommateur(s) suivent la posture de leur serveur ; 2 sans reglage TLS (serveur_icingaweb2, serveur_nextcloud). |
|
|
||||||
| P79 | Replis silencieux : une derivation vide ne passe pas pour un succes | — | ✅ OK | 5 ecosysteme(s) (instance-ci-1646753, OPS-Chezlepro-lab, OPS-Chezlepro, OPS-Technolibre, SITE-Chezlepro) : pattes, edges, certificats, rechargements, jumeaux d' |
|
|
||||||
| P80 | Remise au client : inscrite, nommee, et son second temps a l'heure | — | ✅ OK | Aucun ecosysteme remis a un client : rien a tenir. |
|
|
||||||
| P81 | La console dit sa portee, et ne sert pas un inventaire vide en silence | — | ✅ OK | Portee `poste` derivee des voutes portees, source d'inventaire `instance/inventories/principal/hosts.yml`, et 15 route(s) POST exigent toutes un pouvoir. |
|
|
||||||
| P82 | DNS public : les zones publiees sont servies, signees avant d'etre exposees | — | ✅ OK | 2 zone(s) publique(s) declaree(s), toutes servies par le site ; 2 replication(s) declaree(s) des deux cotes ; transfert ouvert a la cle seule ; aucune expositio |
|
|
||||||
| P83 | Assistants : le registre des runbooks ne prend pas de retard sur le Makefile | — | ✅ OK | 17 runbooks, 139 etapes, 145 cibles documentees : chacune portee par un assistant ou exemptee avec son motif. |
|
|
||||||
|
|
||||||
## Couverture des affirmations ✅ du registre
|
|
||||||
|
|
||||||
Chaque affirmation ✅ automatisable est couverte par la preuve indiquee ci-dessus.
|
|
||||||
Les ✅ **structurelles/doctrinales** non rejouables par une commande (ex. AFF-005
|
|
||||||
`make`=aide, AFF-014 ciblage groupe, AFF-024 `instancier-appliquer`, AFF-051 autorite
|
|
||||||
d'AGENTS.md, AFF-073/075 gardes `make`, AFF-090 wiki) ont ete verifiees a l'audit ;
|
|
||||||
elles restent hors du harnais recurrent (rien d'executable a rejouer).
|
|
||||||
|
|
||||||
## Declarations d'intention (⚪ invérifiables localement — assumees)
|
|
||||||
|
|
||||||
Ces affirmations ne sont pas rejouables hors production ; elles sont **assumees**
|
|
||||||
comme declarations d'intention, non comme preuves :
|
|
||||||
|
|
||||||
- **AFF-036** — « testables avec `--check` autant que possible » : verifiable seulement
|
|
||||||
contre une flotte vivante.
|
|
||||||
- **AFF-091** — contenu pedagogique du wiki : affirmations conceptuelles.
|
|
||||||
- **AFF-096** — « GUI 100 % francais » : revue exhaustive des libelles rendus, non automatisee.
|
|
||||||
- **AFF-007** — hote d'exemple `web-frontal-01` : placeholder assume.
|
|
||||||
|
|
||||||
_Rapport genere le 2026-10-03._
|
|
||||||
|
|
@ -1,130 +0,0 @@
|
||||||
# Preuve de conformite — Set-OPS — 2026-10-09
|
|
||||||
|
|
||||||
> Genere par `make prouver` (`scripts/prouver.py`). **Rejouable** : relancer
|
|
||||||
> reproduit ce rapport. Chaque preuve rejoue l'outillage existant du depot ;
|
|
||||||
> aucune validation n'est reimplementee ici. Voir le mode d'emploi :
|
|
||||||
> [`docs/audit/README.md`](README.md), et le registre trace :
|
|
||||||
> [`docs/audit/affirmations.md`](affirmations.md).
|
|
||||||
|
|
||||||
- **Instance** : `instance` — inventaire `instance/inventories/principal/hosts.yml`
|
|
||||||
- **Verdict** : ✅ CONFORME (94 OK · 0 echec · 0 saute)
|
|
||||||
|
|
||||||
## Preuves
|
|
||||||
|
|
||||||
| # | Preuve | Affirmations | Statut | Detail |
|
|
||||||
|---|---|---|---|---|
|
|
||||||
| P01 | Lint (ansible-lint) | AFF-006 | ✅ OK | } \| to_nice_json }}`. |
|
|
||||||
| P02 | Tests unitaires (inventaire, raser, ecritures du plan, rendu du GUI) | — | ✅ OK | OK |
|
|
||||||
| P03 | Diff-vide du plan — TOUTES les instances | AFF-001, AFF-004, AFF-030, AFF-031, AFF-032 | ✅ OK | 3 instance(s) verifiee(s) — OPS-Chezlepro-lab, OPS-Chezlepro, OPS-Technolibre : plan et inventaire applique coincident. |
|
|
||||||
| P04 | Groupes <-> playbooks homonymes | AFF-008 | ✅ OK | |
|
|
||||||
| P05 | Dependances causales de groupes | AFF-009, AFF-084 | ✅ OK | |
|
|
||||||
| P06 | Validateurs de registres (serveurs/apps/bases/domaines) | AFF-003 | ✅ OK | Registre des domaines valide. |
|
|
||||||
| P07 | GUI (node --check) | AFF-033 | ✅ OK | JS du GUI : syntaxe valide (node --check), 1 nom(s) surveille(s) sans reference orpheline. |
|
|
||||||
| P08 | Orchestration (couches + graphe) | AFF-070 | ✅ OK | Orchestration coherente : 42 groupes classes, aucun cycle, aucune arete en arriere ; playbooks/site.yml a jour. |
|
|
||||||
| P09 | Flux reseau (schema + matrice) | AFF-071 | ✅ OK | Flux coherents : 40 rôles, 119 flux, schéma + matrice OK. |
|
|
||||||
| P10 | Handlers <-> notify | AFF-034, AFF-035 | ✅ OK | Tout notify pointe vers un handler du meme role (49 roles). |
|
|
||||||
| P11 | Syntaxe des playbooks (--syntax-check) | AFF-083 | ✅ OK | serveur_resolveur_site |
|
|
||||||
| P12 | Existence des runbooks cites | AFF-010, AFF-011, AFF-012, AFF-083 | ✅ OK | 17/17 runbooks/registres cites presents. |
|
|
||||||
| P13 | Invariants structurels/doctrinaux | AFF-015, AFF-022, AFF-037, AFF-038, AFF-062 | ✅ OK | LICENSE, socle dossier, pas de couches paralleles, SSH clef-only, nftables off : OK. |
|
|
||||||
| P14 | Pas de chemin lab/ code en dur | AFF-097 | ✅ OK | Aucun chemin instance/inventories/lab/group_vars code en dur. |
|
|
||||||
| P15 | Modele public socle valide | AFF-022, AFF-099 | ✅ OK | Modele public socle : domaines/serveurs/applications/bases valides. |
|
|
||||||
| P16 | Inventaire Ansible complet (--list) | AFF-030 | ✅ OK | 13 hotes, 33 groupes (inventaire dechiffre et parse). |
|
|
||||||
| P17 | Tous les modeles valident (registres + underlay) | AFF-022, AFF-099 | ✅ OK | Les 1 modele(s) decouvert(s) valident. |
|
|
||||||
| P18 | Gabarit de voute complet | AFF-026 | ✅ OK | Gabarit de voute complet : 35 secret(s) exige(s), tous presents. (Voute reelle non lisible ici : verification sautee.) |
|
|
||||||
| P19 | Le GUI couvre le schema du plan | AFF-002, AFF-095 | ✅ OK | GUI : les 30 champ(s) des plans reels sont editables (2 plan(s) inspecte(s)), registres toleres : nomenclature. |
|
|
||||||
| P20 | Adressage 100% derive du seed (aucun stocke) | AFF-001, AFF-003 | ✅ OK | 2 nomenclature(s) et 7 zone(s) de site : adressage 100% derive du seed index. |
|
|
||||||
| P21 | Federation : aucun index en collision | AFF-102 | ✅ OK | Federation coherente : 4 instance(s) federee(s), aucun index en collision. |
|
|
||||||
| P22 | Plan de recette a jour (genere du wiki) | AFF-002 | ✅ OK | Plan de recette à jour (22 sections). |
|
|
||||||
| P23 | Underlay sans collision avec la plage tenant | AFF-103 | ✅ OK | Underlay conforme : 14 reseau(x), aucune collision avec la plage tenant. |
|
|
||||||
| P24 | Frontiere nord/sud : acces d'administration declare | AFF-104 | ✅ OK | note : serveur_powerdns declare un port `derive` que le plan du site ne resout pas — aucune regle emise. |
|
|
||||||
| P25 | Pare-feu Proxmox : est-ouest intra-tenant derive | AFF-107 | ✅ OK | CONFORME : pare-feu Proxmox, 2 tenant(s), 40 groupe(s), 108 regle(s). |
|
|
||||||
| P26 | Integrations universelles : aucun hote laisse de cote | AFF-108 | ✅ OK | 13 hote(s) x 6 integration(s) universelle(s) : aucune lacune, aucune recopie (0 exemption(s) derivee(s) du service rendu). |
|
|
||||||
| P27 | Propriete des intrants : hebergeur et tenant separes | AFF-109 | ✅ OK | 8 cle(s) de cluster chez l'hebergeur, aucune recopiee dans les group_vars du tenant. |
|
|
||||||
| P28 | Pools Proxmox : un par tenant, sans collision | AFF-110 | ✅ OK | CONFORME : 3 pool(s) Proxmox, 35 VM placee(s), aucun nom ni VMID en collision. |
|
|
||||||
| P29 | Authentification : chaque role declare sa position | AFF-111 | ✅ OK | 34 role(s) serveur declares (interne-sans-auth 2, ldap-direct 2, sans-auth-humaine 23, socle-identite 2, web-sso 5) ; 2 lacune(s) nommee(s) : serveur_loki, serv |
|
|
||||||
| P30 | SDN EVPN : zones, VNets et sous-reseaux derives | AFF-112 | ✅ OK | CONFORME : SDN EVPN, 2 zone(s), 12 VNet(s), 12 sous-reseau(x), aucune collision. |
|
|
||||||
| P31 | Documentation : tout ce que le depot FAIT est nomme | — | ✅ OK | 78 scripts expliques et atteignables, 151 cibles make documentees, 69 roles avec README. |
|
|
||||||
| P32 | Intrants exiges par les roles : tous fournis | — | ✅ OK | CONFORME : 37 exigence(s) de role, toutes satisfaites (148 cle(s) declaree(s) par l'instance). |
|
|
||||||
| P33 | Aucune collision de port entre roles co-localises | — | ✅ OK | CONFORME : 44 revendication(s) de port, aucune collision entre roles co-localises (35 groupes). |
|
|
||||||
| P34 | Chaque document declare son lecteur | — | ✅ OK | 50 document(s) declarent leur lecteur (47 genere(s) exempte(s)). |
|
|
||||||
| P35 | Toute application exigeant une base en a une au plan | — | ✅ OK | 4 application(s) exigeant une base l'ont toutes (4 entree(s) au registre). |
|
|
||||||
| P36 | Tout detenteur d'etat porte une sauvegarde | — | ✅ OK | 9 hote(s) de l'ecosysteme et 3 du site detiennent de l'etat, tous porteurs de `client_backup` (11 groupe(s) au catalogue). |
|
|
||||||
| P37 | Le placement du tenant existe chez son hebergeur | — | ✅ OK | placement confronte a l'hebergeur monte (SITE-Chezlepro) : noeud, stockage, pont — tous offerts. |
|
|
||||||
| P38 | Catalogue des services : la carte dit ce que le moteur fait | — | ✅ OK | Catalogue a jour : 42 role(s) serveur/client tous nommes, 42 groupe(s) cite(s) en table existent tous. |
|
|
||||||
| P39 | Glossaire : tout mot employe est enseigne | — | ✅ OK | Glossaire complet : 81 terme(s) du jargon expliques, 15 lien(s) valides, 97 page(s) de wiki toutes atteignables. |
|
|
||||||
| P40 | Parente : l'ecosysteme sait de quoi il descend | — | ✅ OK | Parente coherente : 4 depot(s), tous retrouves, tous porteurs d'un remote. |
|
|
||||||
| P41 | Resolution d'instance : une seule, partagee | — | ✅ OK | Resolution unique : 74 script(s) passent par `inventory_rules`, 3 exemption(s) nommee(s). |
|
|
||||||
| P42 | L'edge porte les noms qu'il publie | — | ✅ OK | 3 edge(s) emettent un certificat portant les noms publies (OPS-Chezlepro-lab/principal, OPS-Chezlepro/principal, OPS-Technolibre/principal). |
|
|
||||||
| P43 | Frontiere : le devis voit les machines du site | — | ✅ OK | Devis de la frontiere : 9 machine(s) du plan retrouvees, 201 regle(s) du site. |
|
|
||||||
| P44 | Integrations : le serveur avant ses clients | — | ✅ OK | 5 integration(s) appliquent leur serveur avant leurs clients. |
|
|
||||||
| P45 | Pare-feu Proxmox : arme sur les VNet SDN, jamais ailleurs | — | ✅ OK | Le pare-feu Proxmox ne s'arme que sur un VNet SDN (4 cas evalues, dont un qui doit rendre VRAI). |
|
|
||||||
| P46 | Plancher /etc/hosts : un seul role en decide | — | ✅ OK | Un seul maitre du plancher — roles/hosts_statiques/tasks/main.yml : manage_etc_hosts: false ; et le gabarit maitre est pose (roles/hosts_statiques/templates/hos |
|
|
||||||
| P47 | Zones inverses : couvrir l'occupe, et rien de plus | — | ✅ OK | Les zones inverses couvrent l'occupe et rien de plus (5 cas evalues, dont un site a quatre zones et un tenant a une). |
|
|
||||||
| P48 | La carte d'orientation designe ce qui existe, et compte juste | — | ✅ OK | La carte designe 102 chemin(s) qui existent, et ses 7 chiffres correspondent a la mesure. |
|
|
||||||
| P49 | Registre des flux : la matrice d'audit est a jour | — | ✅ OK | Le registre des flux reproduit exactement ce que les `meta/flux.yml` declarent (137 lignes). |
|
|
||||||
| P50 | Silences : un refus muet est declare, place en dernier, et motive | — | ✅ OK | 2 silence(s) declare(s), tous en sequence > 1 (la plus haute des 304 regles `pass`), tous non consignes et tous motives. |
|
|
||||||
| P51 | Collections : toutes declarees, toutes epinglees | — | ✅ OK | 3 collection(s) et 2 bibliotheque(s) Python declarees et epinglees : ansible.posix==1.6.2, community.general==10.3.0, community.postgresql==3.10.2 |
|
|
||||||
| P52 | Materialiser n'exige pas d'entrer dans le tenant | — | ✅ OK | `creer-vm` confirme par l'agent invite (API des hyperviseurs, deja utilisee pour creer), sans exiger d'entrer dans le tenant. |
|
|
||||||
| P53 | L'interne refuse a voix haute, la bordure se tait | — | ✅ OK | L'interne parle, la bordure se tait — 13 ruleset(s) nftables refusent a voix haute ; pare-feu est-ouest en REJECT, source unique ; frontiere : WAN muet, interfa |
|
|
||||||
| P54 | L'insemination ne reclame aucun secret du tenant | — | ✅ OK | 2 couche(s) d'insemination (serveur_debian, serveur_ops), 9 role(s) applique(s), aucun secret de tenant reclame. |
|
|
||||||
| P55 | La cle du SITE ne nait que sur le runner d'un tenant | — | ✅ OK | 13 hote(s) : la cle du SITE ne nait que sur 1 runner(s) de tenant, celle du tenant sur 13. |
|
|
||||||
| P56 | Gabarit minimal, et rien de retire n'est perdu | — | ✅ OK | Gabarit minimal : 4 role(s), tous indispensables au premier demarrage ; 14 role(s) retire(s), tous repris par le socle ou le durcissement. |
|
|
||||||
| P57 | Comptes en prose : les chiffres du depot sur lui-meme | — | ✅ OK | Les comptes ecrits en prose correspondent a la mesure (94 preuves, 69 roles, 42 groupes). |
|
|
||||||
| P58 | Habilitations : chaque service dit a quel GROUPE, et par quoi | — | ✅ OK | 8 habilitation(s) declarees, toutes nommant un groupe, un mecanisme connu et une raison ; les `role-realm` sont projetees. |
|
|
||||||
| P59 | Enumerations annoncees : le nombre correspond a ce qui suit | — | ✅ OK | 2 enumeration(s) annoncee(s) correspondent a ce qu'elles annoncent (formes non ambigues seulement). |
|
|
||||||
| P60 | Wiki publie : la forge sert ce que le depot dit | AFF-002 | ✅ OK | Le wiki publie correspond au depot : `wiki/` n'a pas bouge depuis `e9215a2` (publie le 2026-10-07). |
|
|
||||||
| P61 | Schema du plan : il decrit tout ce que les plans contiennent | AFF-033 | ✅ OK | Le schema decrit 56 champ(s) sur 7 registres ; il couvre tout ce que les plans reels contiennent, et la FORME de chaque champ (scalaire / objet / table) corresp |
|
|
||||||
| P62 | Schema du plan : il decrit tout ce que le MOTEUR accepte | AFF-033 | ✅ OK | Les 4 validateurs n'acceptent aucun champ que le schema ignore (applications:8, bases_donnees:4, domaines_publics:9, serveurs:3 champ(s) lus par validateur). |
|
|
||||||
| P63 | cloud-init nait avec la VM et ne lui survit pas | — | ✅ OK | cloud-init est au gabarit (la premiere seconde), absent du socle (pas de va-et-vient), et retire par le durcissement — avec la garde qui verifie que le reseau s |
|
|
||||||
| P64 | Sondes de supervision : declarees ET deposees | — | ✅ OK | 47 sonde(s) declaree(s) ET deposee(s), chacune avec sa raison et son `ttl` : client_journal/journaux, client_metrique/metriques, client_pki/certificat, client_s |
|
|
||||||
| P65 | Depots tiers : demandes au cache, jamais en HTTPS direct | — | ✅ OK | 4 depot(s) tiers relaye(s) par le cache, aucun role ne les vise en https:// ecrit en dur. |
|
|
||||||
| P66 | Clients OIDC : chaque URI vise un nom que le plan expose | — | ✅ OK | 4 client(s) OIDC, toutes leurs URI visent un FQDN que le plan expose (6 exposition(s)). |
|
|
||||||
| P67 | Nom public : le service porte celui du plan, pas celui du role | — | ✅ OK | 13 service(s) expose(s) portent le nom du plan, sur 2 inventaire(s) : instance, SITE. |
|
|
||||||
| P68 | Cle de depot telechargee : mesuree avant d'etre utilisee | — | ✅ OK | 5 role(s) telechargent une cle de depot, tous la mesurent avant de s'en servir. |
|
|
||||||
| P69 | Amorcage d'un tenant : l'adresse designe le site REEL | — | ✅ OK | 2 adresse(s) d'amorcage designent bien une machine du site. |
|
|
||||||
| P70 | Depot de binaires : il tient tout ce que les roles vont chercher | — | ✅ OK | 27 artefact(s) direct(s) tenus par le depot du site. |
|
|
||||||
| P71 | Pool du site : le genome ne nait pas chez un tenant | — | ✅ OK | `site-creer` nomme `--pool-site` ; le pool du genome ne peut plus etre celui d'un tenant. |
|
|
||||||
| P72 | Annuaire : aucun service ne se lie avec le compte du maitre | — | ✅ OK | 5 role(s) consultent l'annuaire, chacun avec SON compte de service ; seul `amorcage_acces` garde celui d'administration, et il provisionne au lieu de consommer. |
|
|
||||||
| P73 | Le locataire designe les services de son site REEL | — | ✅ OK | CONFORME : 10 intrant(s) du locataire concordent avec ce que le site expose (1 non declare(s), donc derive(s) ou non utilise(s)). |
|
|
||||||
| P74 | Gabarit dore : une seule declaration, au plan du site | — | ✅ OK | Gabarit declare une seule fois : VMID 9006 « modeleSetOPS-minimal ». |
|
|
||||||
| P75 | Les parametres de clonage traversent les trois maillons | — | ✅ OK | 14 parametre(s) de clonage, tous emis par l'inventaire. |
|
|
||||||
| P76 | Tout gabarit de role se rend vraiment | — | ✅ OK | 189 gabarits de role : tous se rendent. |
|
|
||||||
| P77 | Panneaux declares : assemblables, et gradues | — | ✅ OK | 8 panneau(x) declare(s) dans 2 role(s), tous avec titre, expression, raison et une unite que la table sait traduire. |
|
|
||||||
| P78 | Un consommateur de base suit le verrou TLS de son serveur | — | ✅ OK | 3 consommateur(s) suivent la posture de leur serveur ; 2 sans reglage TLS (serveur_icingaweb2, serveur_nextcloud). |
|
|
||||||
| P79 | Replis silencieux : une derivation vide ne passe pas pour un succes | — | ✅ OK | 4 ecosysteme(s) (OPS-Chezlepro-lab, OPS-Chezlepro, OPS-Technolibre, SITE-Chezlepro) : pattes, edges, certificats, rechargements, jumeaux d'amorcage, sorties du |
|
|
||||||
| P80 | Remise au client : inscrite, nommee, et son second temps a l'heure | — | ✅ OK | Aucun ecosysteme remis a un client : rien a tenir. |
|
|
||||||
| P81 | La console dit sa portee, et ne sert pas un inventaire vide en silence | — | ✅ OK | Portee `poste` derivee des voutes portees, source d'inventaire `instance/inventories/principal/hosts.yml`, et 15 route(s) POST exigent toutes un pouvoir. |
|
|
||||||
| P82 | DNS public : les zones publiees sont servies, signees avant d'etre exposees | — | ✅ OK | 2 zone(s) publique(s) declaree(s), toutes servies par le site ; 2 replication(s) declaree(s) des deux cotes ; transfert ouvert a la cle seule ; aucune expositio |
|
|
||||||
| P83 | Assistants : le registre des runbooks ne prend pas de retard sur le Makefile | — | ✅ OK | 17 runbooks, 144 etapes, 150 cibles documentees : chacune portee par un assistant ou exemptee avec son motif. |
|
|
||||||
| P84 | La fiche du site dit exactement ce que chaque locataire porte | — | ✅ OK | 2 fiche(s) de site : chacune dit exactement ce que son locataire porte (copies, racine, inventaire genere). |
|
|
||||||
| P85 | La face reseau du locataire dit exactement ce que le site en tire | — | ✅ OK | 2 face(s) reseau : chacune dit exactement ce que le site en tire (decouverte, devis Proxmox, frontiere, DNS public, sauvegarde). |
|
|
||||||
| P86 | Machine par machine, le locataire et Proxmox admettent les memes entrees | — | ✅ OK | 2 locataire(s) : machine par machine et port par port, nftables et Proxmox admettent les memes sources. |
|
|
||||||
| P87 | La frontiere designe chaque locataire par les identites qu'il publie | — | ✅ OK | 2 locataire(s) : la frontiere les designe par les identites qu'ils publient (supernet, administration, tunnel, groupes, routes, sortie). |
|
|
||||||
| P88 | Ce qui entre depuis l'Internet chez un locataire, la frontiere le tient de lui | — | ✅ OK | 2 locataire(s) : regles WAN, redirections et tunnel disent exactement ce que leur face reseau ouvre a tous. |
|
|
||||||
| P89 | Ce que l'administration atteint chez un locataire, la frontiere le tient de lui | — | ✅ OK | 2 locataire(s) : par la gestion, le VPN et le tunnel, l'administration atteint exactement ce que leur face reseau lui ouvre. |
|
|
||||||
| P90 | Ce qui sort d'un locataire vers l'Internet, la frontiere le tient de lui | — | ✅ OK | 2 locataire(s) : la frontiere laisse sortir exactement ce que leur face reseau publie. |
|
|
||||||
| P91 | La fiche du site est deposee a jour, et l'inventaire se genere sans le site | — | ✅ OK | 2 locataire(s) : fiche du site deposee a jour, et inventaire genere sans le site identique a l'octet pres. |
|
|
||||||
| P92 | Chaque locataire a publie sa face reseau a jour | — | ✅ OK | 2 locataire(s) : face reseau publiee, a jour. |
|
|
||||||
| P93 | La face publie, machine par machine, les parametres de clonage de l'inventaire | — | ✅ OK | 26 machine(s) : la face publie exactement les parametres de clonage de l'inventaire. |
|
|
||||||
| P94 | Creer, raser et placer un locataire par sa face visent les memes machines | — | ✅ OK | 2 locataire(s) : creer, raser et placer visent par la face les memes machines que par l'instance montee. |
|
|
||||||
|
|
||||||
## Couverture des affirmations ✅ du registre
|
|
||||||
|
|
||||||
Chaque affirmation ✅ automatisable est couverte par la preuve indiquee ci-dessus.
|
|
||||||
Les ✅ **structurelles/doctrinales** non rejouables par une commande (ex. AFF-005
|
|
||||||
`make`=aide, AFF-014 ciblage groupe, AFF-024 `instancier-appliquer`, AFF-051 autorite
|
|
||||||
d'AGENTS.md, AFF-073/075 gardes `make`, AFF-090 wiki) ont ete verifiees a l'audit ;
|
|
||||||
elles restent hors du harnais recurrent (rien d'executable a rejouer).
|
|
||||||
|
|
||||||
## Declarations d'intention (⚪ invérifiables localement — assumees)
|
|
||||||
|
|
||||||
Ces affirmations ne sont pas rejouables hors production ; elles sont **assumees**
|
|
||||||
comme declarations d'intention, non comme preuves :
|
|
||||||
|
|
||||||
- **AFF-036** — « testables avec `--check` autant que possible » : verifiable seulement
|
|
||||||
contre une flotte vivante.
|
|
||||||
- **AFF-091** — contenu pedagogique du wiki : affirmations conceptuelles.
|
|
||||||
- **AFF-096** — « GUI 100 % francais » : revue exhaustive des libelles rendus, non automatisee.
|
|
||||||
- **AFF-007** — hote d'exemple `web-frontal-01` : placeholder assume.
|
|
||||||
|
|
||||||
_Rapport genere le 2026-10-09._
|
|
||||||
|
|
@ -288,15 +288,6 @@
|
||||||
"description": "Le GROUPE Ansible qui sert cette zone (ex. serveur_nginx).",
|
"description": "Le GROUPE Ansible qui sert cette zone (ex. serveur_nginx).",
|
||||||
"x-source-valeurs": "groupes_edge"
|
"x-source-valeurs": "groupes_edge"
|
||||||
},
|
},
|
||||||
"resolution_interne": {
|
|
||||||
"type": "string",
|
|
||||||
"title": "Resolution interne",
|
|
||||||
"description": "`service` : le plancher et la zone menent au service lui-meme, l'edge ne sert qu'aux personnes.",
|
|
||||||
"enum": [
|
|
||||||
"edge",
|
|
||||||
"service"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"secondaires": {
|
"secondaires": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"title": "Secondaires",
|
"title": "Secondaires",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
# Ecrit par `make wiki-publier`, lu par la preuve P60. Ne pas editer a la main.
|
# Ecrit par `make wiki-publier`, lu par la preuve P60. Ne pas editer a la main.
|
||||||
remote: ssh://git@eregion.chezlepro.ca:2222/Alliance-Boreale/Set-OPS-Public.wiki.git
|
remote: ssh://git@eregion.chezlepro.ca:2222/Alliance-Boreale/Set-OPS-Public.wiki.git
|
||||||
source: e9215a2
|
source: 2bbe122
|
||||||
date: 2026-10-07
|
date: 2026-09-29
|
||||||
|
|
|
||||||
|
|
@ -25,8 +25,8 @@ README de rôles). Cette page comble ces deux trous.
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| rôles | 69 | `roles/*/` |
|
| rôles | 69 | `roles/*/` |
|
||||||
| README de rôles | 69 | `roles/*/README.md` — l'écart avec la ligne au-dessus est la dette |
|
| README de rôles | 69 | `roles/*/README.md` — l'écart avec la ligne au-dessus est la dette |
|
||||||
| documents | 46 | `docs/*.md` |
|
| documents | 45 | `docs/*.md` |
|
||||||
| pièces d'audit | 52 | `docs/audit/*` |
|
| pièces d'audit | 48 | `docs/audit/*` |
|
||||||
| unités de wiki | 27 | `wiki/*.md` |
|
| unités de wiki | 27 | `wiki/*.md` |
|
||||||
| décisions en vigueur | 85 | lignes `\| **D-nn** \|` de `decisions-architecture.md` |
|
| décisions en vigueur | 85 | lignes `\| **D-nn** \|` de `decisions-architecture.md` |
|
||||||
| décisions renversées | 3 | lignes `\| **D-nn** —` du même document |
|
| décisions renversées | 3 | lignes `\| **D-nn** —` du même document |
|
||||||
|
|
@ -36,7 +36,7 @@ README de rôles). Cette page comble ces deux trous.
|
||||||
| Sujet | Documents |
|
| Sujet | Documents |
|
||||||
|---|---|
|
|---|---|
|
||||||
| **Autorité / gouvernance** | `AGENTS.md` (source d'autorité), `CLAUDE.md`, `docs/MISE-A-JOUR-CODEX-CLAUDE.md` |
|
| **Autorité / gouvernance** | `AGENTS.md` (source d'autorité), `CLAUDE.md`, `docs/MISE-A-JOUR-CODEX-CLAUDE.md` |
|
||||||
| **Le modèle (plan)** | `docs/architecture-set-ops.md` (survol) → `docs/plan-et-generation.md` (à fond) → `docs/meta-classe.md` (concept) → `docs/conception-contextes.md` (site et locataire : deux classes, un contrat) |
|
| **Le modèle (plan)** | `docs/architecture-set-ops.md` (survol) → `docs/plan-et-generation.md` (à fond) → `docs/meta-classe.md` (concept) |
|
||||||
| **Services, maturité, dette** | `docs/catalogue-services.md` (**la carte de maturité + la cruft y sont déjà**) |
|
| **Services, maturité, dette** | `docs/catalogue-services.md` (**la carte de maturité + la cruft y sont déjà**) |
|
||||||
| **Exploitation / VM** | `docs/vm-lifecycle.md`, `docs/procedure-template-debian13-proxmox.md`, `docs/config-proxmox.md`, `docs/nomenclature-vm.md`, `docs/multi-instances.md` |
|
| **Exploitation / VM** | `docs/vm-lifecycle.md`, `docs/procedure-template-debian13-proxmox.md`, `docs/config-proxmox.md`, `docs/nomenclature-vm.md`, `docs/multi-instances.md` |
|
||||||
| **Conceptions de domaine** | `docs/identite-sso.md`, `docs/courriel-conception.md`, `docs/bindings-conception.md`, `docs/dns-interne.md`, `docs/dimensionnement-ressources.md`, `docs/integrations-vm.md` |
|
| **Conceptions de domaine** | `docs/identite-sso.md`, `docs/courriel-conception.md`, `docs/bindings-conception.md`, `docs/dns-interne.md`, `docs/dimensionnement-ressources.md`, `docs/integrations-vm.md` |
|
||||||
|
|
|
||||||
|
|
@ -1,277 +0,0 @@
|
||||||
# Contextes : un tronc commun, deux classes (SITE et LOCATAIRE)
|
|
||||||
|
|
||||||
> **Pour qui :** le **mainteneur** — comment le moteur sait s'il sert un site ou un locataire, et ce que les deux s'apprennent l'un à l'autre.
|
|
||||||
|
|
||||||
> **Statut : arrêtée avec l'exploitant le 2026-10-04.** Rien n'est encore construit ; les
|
|
||||||
> décisions sont au §7, le chemin au §6.
|
|
||||||
|
|
||||||
## 1. Le problème, mesuré
|
|
||||||
|
|
||||||
Le moteur ne sait pas dans quel contexte il tourne : **chaque script le devine**. Relevé du
|
|
||||||
2026-10-04 : **33 scripts** font leur propre déduction, à partir de cinq indices différents.
|
|
||||||
|
|
||||||
| Indice | Ce qu'on en déduit | Scripts |
|
|
||||||
|---|---|---|
|
|
||||||
| lien `instance/` ou `SETOPS_INSTANCE` | « un locataire est monté » | 23 |
|
|
||||||
| lien `underlay.yml` ou `SETOPS_UNDERLAY` | « un site est monté » ; son plan est à côté | 8 |
|
|
||||||
| `SETOPS_INVENTAIRE` | quel inventaire de locataire lire | 8 |
|
|
||||||
| `../*/plan/nomenclature.yml` | « la fédération », les locataires frères | 11 |
|
|
||||||
| `../SITE-*/underlay.yml` | « les sites » | 2 |
|
|
||||||
|
|
||||||
Les indices ne concordent pas toujours, et chaque désaccord a déjà produit un défaut silencieux :
|
|
||||||
|
|
||||||
- **2026-08-14** : `frontiere-plan` voulait poser sur la frontière de Technolibre les règles de
|
|
||||||
Chezlepro. « La fédération » valait « les locataires de ce site », jusqu'au second site.
|
|
||||||
- **2026-09-16** : la console du runner du site affichait zéro machine, sans erreur. Elle
|
|
||||||
cherchait un inventaire de locataire là où il n'y en a pas.
|
|
||||||
- **2026-10-04** : `make ci`, sur le poste, mélangeait le modèle public et les écosystèmes
|
|
||||||
réels. P74 lisait `SETOPS_UNDERLAY` (le modèle) ; P82 lisait le lien `underlay.yml` et les
|
|
||||||
dossiers frères (le site réel).
|
|
||||||
|
|
||||||
Les rôles Ansible, eux, ne posent pas ce problème : ils sont **déjà** le tronc commun. Un même
|
|
||||||
`serveur_postgresql` sert au site et chez un locataire.
|
|
||||||
|
|
||||||
## 2. Le modèle
|
|
||||||
|
|
||||||
```
|
|
||||||
Ecosysteme (tronc commun)
|
|
||||||
/ \
|
|
||||||
Site Locataire
|
|
||||||
\ /
|
|
||||||
`-- contrat --' (associations : un site A des locataires,
|
|
||||||
un locataire A un site)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2.1 Le tronc commun : `Ecosysteme`
|
|
||||||
|
|
||||||
Ce que tout écosystème possède, quel que soit son contexte :
|
|
||||||
|
|
||||||
- un **nom** et un **dépôt** (`SITE-Chezlepro`, `OPS-Technolibre`) ;
|
|
||||||
- une **voûte** et sa clé (`~/.config/setops-vault-<dépôt>`) ;
|
|
||||||
- un **plan** (`<dépôt>/plan/`) et un **index**, dont dérive son adressage (le site
|
|
||||||
aussi depuis le 2026-09-20) ;
|
|
||||||
- des **machines**, déployées par les **mêmes rôles** : socle, durcissement, PKI, journaux,
|
|
||||||
métriques, supervision, sauvegarde de son propre état ;
|
|
||||||
- une **filiation** : le moteur et le commit dont il descend ;
|
|
||||||
- les **preuves communes** : lint, rendu des gabarits, adressage dérivé, etc.
|
|
||||||
|
|
||||||
Méthodes abstraites, que chaque classe **surcharge** : `inventaire()`, `machines()`,
|
|
||||||
`preuves()`, `verbes()`, `console()`.
|
|
||||||
|
|
||||||
### 2.2 `Site(Ecosysteme)`
|
|
||||||
|
|
||||||
- **Déclaration** : `underlay.yml` (le matériel, les réseaux `site` et `fabric`) et `plan/`
|
|
||||||
(`10-intrants.yml`, serveurs, applications, domaines, bases).
|
|
||||||
- **Inventaire** : dynamique (`site_inventaire.py`). Le site ne dérive rien d'un plan de services ;
|
|
||||||
sa déclaration est sa forme finale.
|
|
||||||
- **Ce qu'il porte en propre** : le matériel (hyperviseurs, commutateurs, frontière),
|
|
||||||
la matérialisation des VM (Proxmox), le SDN, le pare-feu Proxmox, la frontière OPNsense,
|
|
||||||
le DNS public, le dépôt des sauvegardes des locataires, le cache et les artefacts, la forge
|
|
||||||
du génome.
|
|
||||||
- **Relation** : `locataires()`, la liste de `underlay.tenants` résolue en objets
|
|
||||||
`Locataire`. Le site n'en lit que la **face réseau** (§2.4).
|
|
||||||
|
|
||||||
### 2.3 `Locataire(Ecosysteme)`
|
|
||||||
|
|
||||||
- **Déclaration** : `plan/` (nomenclature, serveurs, applications, bases, domaines).
|
|
||||||
- **Inventaire** : généré (`instancier.py` → `hosts.yml`). C'est la **méta-classe** de
|
|
||||||
[`meta-classe.md`](meta-classe.md) : une définition qui engendre toute la flotte.
|
|
||||||
- **Ce qu'il porte en propre** : la configuration de ses services, la remise au client.
|
|
||||||
- **Relation** : `site()`, l'hébergeur que nomme `parente.yml`, résolu en objet `Site`. Le
|
|
||||||
locataire n'en lit que les **intrants exposés** (§2.4).
|
|
||||||
|
|
||||||
### 2.4 Ce que le site et le locataire s'apprennent l'un à l'autre
|
|
||||||
|
|
||||||
Les deux entités **s'informent mutuellement**. Relevé du 2026-10-04 : qui décide de chaque
|
|
||||||
information, où elle vit, et comment elle parvient à l'autre.
|
|
||||||
|
|
||||||
**Ce que chacun a sous la main.** Le runner du site porte le moteur, son dépôt **et ceux de
|
|
||||||
ses locataires** (sans leurs voûtes). Le runner d'un locataire ne porte que le moteur et
|
|
||||||
**son propre** dépôt. Le poste porte tout.
|
|
||||||
|
|
||||||
#### Le site informe le locataire, par trois canaux
|
|
||||||
|
|
||||||
**Canal 1 : des copies écrites à la main** dans le dépôt du locataire.
|
|
||||||
|
|
||||||
| Information | Décidée par | Tenue chez le site dans | Copiée chez le locataire dans | Contrôle |
|
|
||||||
|---|---|---|---|---|
|
|
||||||
| son **index** | le site | `underlay.yml` → `tenants` | `plan/nomenclature.yml` (`index`) | `underlay valider` |
|
|
||||||
| son **adresse publique** | le site | `opnsense.yml` → `opnsense_ips_publiques` | `10-intrants.yml` (`ip_publique`) | — |
|
|
||||||
| les **10 intrants de service** : résolveur, cache, binaires, forge du génome, cible de sauvegarde, DNS public, plan d'administration, passerelle | le site (dérivés de son plan, par `site_intrants.py`) | son plan | `10-intrants.yml`, et `serveur_ops.yml` pour la forge | `site_intrants.py --verifier`, seulement là où les deux dépôts sont présents (le poste) |
|
|
||||||
| sa **racine de confiance** | le site | `ac-racine-site.crt` | le même fichier, copié | — |
|
|
||||||
| *hors contrat* : un dépôt de la forge du site désigné par son adresse | — | — | `serveur_web_dorsal.yml` (Chezlepro) | **aucun** |
|
|
||||||
|
|
||||||
**Canal 2 : une lecture directe, au moment de générer l'inventaire.** `instancier.py` ouvre
|
|
||||||
l'`underlay.yml` et le plan du site pour écrire le `hosts.yml` du locataire. Mesuré sur
|
|
||||||
Technolibre, inventaire généré avec puis sans le site monté : **quatre variables changent**.
|
|
||||||
|
|
||||||
| Variable du locataire | Avec le site monté | Sans le site |
|
|
||||||
|---|---|---|
|
|
||||||
| `chrony_serveurs` | `10.0.4.1` (la frontière) | absente |
|
|
||||||
| `proxmox_pont` | `t23appl` (le VNet SDN) | absente |
|
|
||||||
| `proxmox_etiquette_vlan` | aucune (le SDN étiquette) | `1236` |
|
|
||||||
| `serveur_resolveur_zones_deleguees` | `genese.internal` → `10.37.34.11` | absente |
|
|
||||||
|
|
||||||
**Canal 3 : le réseau.** Le runner du locataire **tire** son génome de la forge du site ; il est
|
|
||||||
né de l'**insémination** par le runner du site.
|
|
||||||
|
|
||||||
#### Le locataire informe le site : le site lit et recalcule
|
|
||||||
|
|
||||||
| Information | Décidée par | Tenue chez le locataire dans | Parvient au site par |
|
|
||||||
|---|---|---|---|
|
|
||||||
| ses **zones** et son adressage | dérivés de l'index | `plan/nomenclature.yml` | le site **lit le fichier** |
|
|
||||||
| les **VM à matérialiser** | le locataire | `plan/serveurs.yml` → `hosts.yml` | le site **lit les fichiers** (placement, clonage, pools, SDN) |
|
|
||||||
| ses **flux** | ses rôles et son plan | `meta/flux.yml` des rôles (moteur), croisés avec son `hosts.yml` | le site **recalcule** lui-même, avec **sa** version du moteur et la totalité de l'inventaire du locataire → frontière, NAT, pare-feu Proxmox |
|
|
||||||
| ses **domaines publics** | le locataire | `plan/domaines.yml` (+ `applications.yml`, `serveurs.yml`) | le site **lit les fichiers** → DNS public secondaire |
|
|
||||||
| sa **clé de sauvegarde** | le locataire | `inventories/*/group_vars/serveur_backup.yml` | le site **lit le fichier** → compte Unix sur le dépôt |
|
|
||||||
| ses **accès d'administration** | le locataire | `plan/acces.yml`, `nftables_admin_ssh` | le site **lit les fichiers** → pairs WireGuard, règles d'administration |
|
|
||||||
|
|
||||||
Le SDN, lui, ne prend aucun flux : il ne filtre pas. Il ne reçoit que l'index, dont il dérive
|
|
||||||
la zone, les 6 VNets et les 6 sous-réseaux.
|
|
||||||
|
|
||||||
#### À l'exécution, entre machines
|
|
||||||
|
|
||||||
Ces échanges-là passent par le réseau, pas par les dépôts. Ils sont déjà déclarés en flux :
|
|
||||||
le locataire **dépose** ses sauvegardes chez le site (SFTP), **tire** ses paquets, ses binaires
|
|
||||||
et son génome, **entre** par le tunnel d'administration du site ; le site **réplique** les
|
|
||||||
zones publiques du locataire (AXFR signé TSIG).
|
|
||||||
|
|
||||||
#### Ce que le relevé montre
|
|
||||||
|
|
||||||
1. **Le site fouille l'intérieur du locataire.** Six fichiers de son plan et de son inventaire,
|
|
||||||
`group_vars` compris. Rien ne dit ce que le locataire **accepte** de montrer. Renommer un
|
|
||||||
champ chez le locataire casse le site sans bruit.
|
|
||||||
2. **Les flux sont calculés deux fois**, par le locataire pour ses `nftables` et par le site pour
|
|
||||||
la frontière et Proxmox, chacun avec **sa** version du moteur. Ils concordent tant que les deux
|
|
||||||
runners tiennent le même commit (c'était le cas le 2026-10-04), mais rien ne l'impose.
|
|
||||||
3. **Le locataire vit de copies** : douze valeurs et un certificat, recopiés à la main, plus une
|
|
||||||
valeur hors contrat. La garde qui compare ne tourne que sur le poste ; le runner du locataire
|
|
||||||
ne peut pas savoir que sa copie a vieilli.
|
|
||||||
4. **L'inventaire d'un locataire dépend du site monté au moment de le générer.** Généré sur le
|
|
||||||
runner du locataire, qui n'a pas le dépôt du site, il perdrait son serveur de temps, son SDN
|
|
||||||
et sa délégation DNS. Ça ne s'est jamais vu, parce que l'inventaire est toujours généré sur le
|
|
||||||
poste puis versionné.
|
|
||||||
5. **Deux décisions du site** (l'index, l'adresse publique) vivent en double.
|
|
||||||
|
|
||||||
#### Proposition : deux fiches, une dans chaque sens
|
|
||||||
|
|
||||||
Chacun **publie** ce qu'il donne à l'autre, dans une fiche **générée** par le moteur, jamais
|
|
||||||
écrite à la main. Chacun ne lit que la fiche que l'autre lui destine. Plus aucune lecture
|
|
||||||
croisée, plus aucun recalcul.
|
|
||||||
|
|
||||||
- **La fiche du site pour un locataire.** Tout ce que le site lui **attribue** (index, adresse
|
|
||||||
publique) et lui **offre** : les 10 intrants, sa racine de confiance, et ce que l'instancier
|
|
||||||
allait lire en douce (serveur de temps, délégation DNS, mode SDN et nom des VNets). Une fiche
|
|
||||||
**par locataire** : aucun ne voit le plan du site ni ses voisins. L'instancier ne lit plus que
|
|
||||||
cette fiche, et l'inventaire devient **identique où qu'on le génère**.
|
|
||||||
- **La face réseau du locataire.** Ce qu'il **demande** au site : VM à matérialiser, zones,
|
|
||||||
domaines publics, clé de sauvegarde publique, accès d'administration, et **ses flux déjà
|
|
||||||
résolus** (adresses, ports, protocoles), ceux avec l'extérieur pour la frontière et ceux de
|
|
||||||
chaque VM pour Proxmox. Le locataire génère déjà ses flux résolus (`flux-genere/*.nft`,
|
|
||||||
`*.connectivite.json`) : la face réseau en est la partie destinée au site. Le site ne
|
|
||||||
recalcule plus rien : il applique ce que le locataire publie, après l'avoir confronté à sa
|
|
||||||
propre politique.
|
|
||||||
- **Chaque fiche porte l'empreinte de sa source**, et une preuve de chaque côté vérifie que la
|
|
||||||
fiche reçue correspond à ce que l'autre a publié.
|
|
||||||
|
|
||||||
**Où en est l'étape 2 (2026-10-04).** La fiche du site (P84), les faits de la face réseau
|
|
||||||
(P85) et les flux de chaque machine (P86) existent, et disent exactement ce que les lectures
|
|
||||||
croisées produisent. La première mesure des flux a trouvé une information que le locataire
|
|
||||||
jetait : les clients nommés d'un port aussi public, que Proxmox doit admettre nommément. Il
|
|
||||||
les publie désormais (`sources_declarees`). La frontière, en trois temps : les identités
|
|
||||||
(P87), les entrées publiques (P88), l'administration (P89) et les sorties (P90) sont faites.
|
|
||||||
**L'étape 2 est terminée** (2026-10-05). Étape 3 : l'instancier lit la fiche déposée par le site, et
|
|
||||||
l'inventaire d'un locataire se génère sans son site, à l'octet près (P91). Le locataire publie sa face
|
|
||||||
réseau (P92) ; les comptes de sauvegarde, le DNS public, le pare-feu Proxmox, la frontière et la
|
|
||||||
découverte des locataires du site la lisent. La matérialisation aussi : la face publie les
|
|
||||||
paramètres de clonage (P93) ; `locataire-creer`, `locataire-raser` et `placement-plan TENANT=`
|
|
||||||
nomment leur locataire au lieu de le monter, et visent les mêmes machines, à l'argument près de
|
|
||||||
la ligne `ansible-playbook` (P94, `test_appels_locataire.py`) ; `reconstruire-locataire` les
|
|
||||||
emploie. Reste : la preuve par reconstruction.
|
|
||||||
|
|
||||||
Méthodes du contrat : `site.fiche_pour(locataire)`, `locataire.face_reseau()`. Une classe
|
|
||||||
n'ouvre jamais les fichiers de l'autre ; une preuve vérifiera la règle.
|
|
||||||
|
|
||||||
#### Ce que les fiches donnent : la portabilité
|
|
||||||
|
|
||||||
Un locataire qui change de site, pour un déménagement, un plan de reprise ou une émancipation,
|
|
||||||
n'a plus qu'à **recevoir la fiche de son nouveau site**. Son dépôt ne contient plus rien
|
|
||||||
d'interne à l'ancien : ni copie d'adresse, ni inventaire généré avec l'ancien site monté. En
|
|
||||||
face, le nouveau site n'a qu'à lire sa **face réseau**. Aujourd'hui, la même bascule demande de
|
|
||||||
corriger des copies dans plusieurs fichiers, puis de régénérer l'inventaire avec le nouveau site
|
|
||||||
monté sur le poste.
|
|
||||||
|
|
||||||
## 3. Le poste : un sélecteur
|
|
||||||
|
|
||||||
Aujourd'hui, le poste monte les deux contextes **en même temps** (`instance/` + `underlay.yml`),
|
|
||||||
et `ConsolePoste` hérite de `ConsoleLocataire`. Désormais :
|
|
||||||
|
|
||||||
- **Le poste choisit un contexte actif** : un site **ou** un locataire. La console ouvre celui-là,
|
|
||||||
et seulement celui-là.
|
|
||||||
- **Une opération qui traverse les deux** nomme ses objets au lieu de les deviner.
|
|
||||||
`reconstruire-locataire` en est l'exemple : le site matérialise, puis le locataire monte.
|
|
||||||
L'orchestration devient `site.materialiser(locataire)` puis `locataire.monter()`.
|
|
||||||
- **Les runners ne changent pas** : celui du site n'a qu'un `Site`, celui d'un locataire qu'un
|
|
||||||
`Locataire`. Leur contexte est désormais **dit**, plus déduit.
|
|
||||||
|
|
||||||
## 4. Qui surcharge quoi
|
|
||||||
|
|
||||||
| Méthode | Tronc commun | Site | Locataire |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `inventaire()` | abstraite | script dynamique (`underlay.yml`) | `hosts.yml` généré du plan |
|
|
||||||
| `machines()` | abstraite | VM du site + équipements | VM du plan |
|
|
||||||
| `adressage()` | dérivé de l'index | zones `site` dérivées, liens `fabric` écrits | 6 zones dérivées |
|
|
||||||
| `sauvegardes()` | son propre état, vérifié par restauration | + héberge les dépôts des locataires | dépose chez son site |
|
|
||||||
| `supervision()` | sondes déclarées par les rôles | + matériel, fabric, frontière | — |
|
|
||||||
| `raser()` / `reconstruire()` | — | `site_raser.py` | `raser.py`, `reconstruire_locataire.py` |
|
|
||||||
| `preuves()` | preuves communes | + preuves de site (P23, P74, P82…) | + preuves de locataire |
|
|
||||||
| `verbes()` | `verifier`, `publier`… | `site-*`, `frontiere-*`, `proxmox-*` | `appliquer`, `instancier`, `remise-*` |
|
|
||||||
| `console()` | — | console SITE | console LOCATAIRE |
|
|
||||||
|
|
||||||
## 5. Ce qui ne change pas
|
|
||||||
|
|
||||||
- Les **rôles Ansible**, déjà communs.
|
|
||||||
- Les **formats de plan** : pas dans ce chantier.
|
|
||||||
- La **doctrine** d'`AGENTS.md`.
|
|
||||||
|
|
||||||
## 6. Le chemin, chaque pas prouvé avant le suivant
|
|
||||||
|
|
||||||
1. **`scripts/contexte.py`** : `Ecosysteme`, `Site`, `Locataire`, `contexte_actif()`,
|
|
||||||
`Site.charger(nom)`, `Locataire.charger(nom)`. Tests unitaires. Rien ne l'utilise encore.
|
|
||||||
2. **Les deux fiches**, générées à côté de l'existant sans rien remplacer :
|
|
||||||
`site.fiche_pour(locataire)` et `locataire.face_reseau()`. Une preuve vérifie que chaque
|
|
||||||
fiche dit **exactement** ce que les lectures croisées d'aujourd'hui produisent.
|
|
||||||
3. **Les consommateurs basculent sur les fiches**, un par un : l'instancier sur la fiche du site
|
|
||||||
(l'inventaire généré doit rester identique, octet pour octet) ; la frontière, Proxmox, le DNS
|
|
||||||
public et les comptes de sauvegarde sur la face réseau (chaque devis doit rester inchangé).
|
|
||||||
4. **`prouver.py`** : chaque preuve déclare son contexte (commun, site, locataire) et reçoit son
|
|
||||||
écosystème du module.
|
|
||||||
5. **Les autres scripts**, un par un, vérifiés par `make verifier` et par un devis inchangé.
|
|
||||||
6. **Une preuve « aucune devinette, aucune lecture croisée »** : les indices du §1, et toute
|
|
||||||
ouverture d'un fichier de l'autre contexte, interdits hors de `contexte.py`.
|
|
||||||
7. **Les verbes du Makefile** rangés par contexte.
|
|
||||||
8. **Les consoles** : le sélecteur et deux consoles (chantier suivant).
|
|
||||||
9. **OPS-Modele** : un locataire modèle, et sans doute un site modèle, vérifiés chacun dans son
|
|
||||||
contexte.
|
|
||||||
|
|
||||||
Après les étapes 3 et 5, une reconstruction prouve que la flotte n'a pas bougé.
|
|
||||||
|
|
||||||
## 7. Décisions et questions ouvertes
|
|
||||||
|
|
||||||
### Tranché par l'exploitant
|
|
||||||
|
|
||||||
- **Deux classes, `Site` et `Locataire`, qui héritent d'un tronc commun** (2026-10-04).
|
|
||||||
- **Le poste est un sélecteur** : un contexte actif à la fois (2026-10-04).
|
|
||||||
- **On commence par le moteur**, les consoles viennent ensuite (2026-10-04).
|
|
||||||
- **Le site dépose sa fiche dans le dépôt du locataire** (2026-10-04), comme il y amorce déjà
|
|
||||||
son runner. Le runner du locataire n'a besoin d'aucun accès au dépôt du site, et ne voit
|
|
||||||
ni le plan du site ni ses voisins.
|
|
||||||
|
|
||||||
- **Le contexte actif se nomme dans un fichier `contexte` explicite** (2026-10-04), une seule
|
|
||||||
valeur : `site:SITE-Chezlepro` ou `locataire:OPS-Technolibre`. Un sélecteur qui monte deux
|
|
||||||
liens à la fois contredirait sa propre règle. Les liens `instance/` et `underlay.yml` restent
|
|
||||||
le temps de la bascule, lus par le seul `contexte.py`.
|
|
||||||
- **Un modèle SITE public, `SITE-Modele`**, à côté d'`OPS-Modele` (2026-10-04). Sans site, la
|
|
||||||
CI ne peut exercer ni les preuves de site (P74, P81, P82) ni la fiche que le site dépose chez
|
|
||||||
le locataire.
|
|
||||||
- **Le site a aussi son `parente.yml`** (2026-10-04) : la filiation est dans le tronc commun.
|
|
||||||
|
|
@ -30,7 +30,7 @@ make placement-plan # chaque VM est-elle là où le plan la met
|
||||||
|
|
||||||
## Le trou qu'il comble
|
## Le trou qu'il comble
|
||||||
|
|
||||||
`scripts/prouver.py` porte 94 preuves (dont une conditionnelle, sautée sans la clé de la voûte). Elles sont toutes **statiques** : elles lisent le
|
`scripts/prouver.py` porte 83 preuves (dont une conditionnelle, sautée sans la clé de la voûte). Elles sont toutes **statiques** : elles lisent le
|
||||||
dépôt. Zéro appel réseau, zéro SSH, zéro `ansible`. Elles établissent que le dépôt est
|
dépôt. Zéro appel réseau, zéro SSH, zéro `ansible`. Elles établissent que le dépôt est
|
||||||
cohérent **avec lui-même** — que les handlers existent, que les intrants ont un
|
cohérent **avec lui-même** — que les handlers existent, que les intrants ont un
|
||||||
propriétaire, que rien n'est codé en dur.
|
propriétaire, que rien n'est codé en dur.
|
||||||
|
|
|
||||||
|
|
@ -66,31 +66,6 @@ Un **résolveur** (miroir de `instancier`) agrège, par serveur, les `flux.yml`
|
||||||
- **le registre d'audit** : `docs/registre-flux.md` (généré) + une cible `make flux`.
|
- **le registre d'audit** : `docs/registre-flux.md` (généré) + une cible `make flux`.
|
||||||
|
|
||||||
## Activation prudente
|
## Activation prudente
|
||||||
|
|
||||||
### Descriptions conservées par les pare-feux
|
|
||||||
|
|
||||||
La `raison` non vide du flux et son rôle alimentent les champs natifs : `comment`
|
|
||||||
pour nftables et Proxmox, `description` pour le filtrage OPNsense (`descr` pour ses
|
|
||||||
redirections). Un commentaire de fichier `# ...` ne survit pas au chargement nftables.
|
|
||||||
Les règles communes (administration, connexions établies, refus) portent aussi leur motif.
|
|
||||||
|
|
||||||
Les libellés sont sur une ligne. Le commentaire nftables est borné à 127 octets UTF-8 ;
|
|
||||||
OPNsense garde sa clé `setops:...` complète et borne le texte qui suit pour rester dans
|
|
||||||
255 octets. Une raison abrégée reste disponible en entier dans le registre des flux.
|
|
||||||
Le commentaire Proxmox conserve aussi le pair nommé dans le devis.
|
|
||||||
|
|
||||||
Une correction de description apparaît dans `make proxmox-fw-plan` et
|
|
||||||
`make frontiere-plan`. Son application met à jour le champ sur la règle existante,
|
|
||||||
puis le relit : elle ne recrée pas la règle et ne modifie ni son activation ni sa
|
|
||||||
politique. Les confirmations habituelles restent requises pour appliquer ces devis.
|
|
||||||
`make test` vérifie notamment cette convergence et les limites des champs.
|
|
||||||
|
|
||||||
Références des champs natifs : [nftables](https://netfilter.org/projects/nftables/manpage.html),
|
|
||||||
[API Proxmox](https://github.com/proxmox/pve-firewall/blob/master/src/PVE/API2/Firewall/Rules.pm),
|
|
||||||
[descriptions OPNsense](https://github.com/opnsense/core/blob/master/src/opnsense/mvc/app/models/OPNsense/Base/FieldTypes/DescriptionField.php).
|
|
||||||
|
|
||||||
### Activer le filtrage
|
|
||||||
|
|
||||||
Activer nftables = **action destructive** (peut couper l'accès) → confirmation explicite +
|
Activer nftables = **action destructive** (peut couper l'accès) → confirmation explicite +
|
||||||
déploiement graduel (garder l'accès SSH/Ansible, tester par nœud).
|
déploiement graduel (garder l'accès SSH/Ansible, tester par nœud).
|
||||||
|
|
||||||
|
|
@ -155,3 +130,4 @@ ignorée, ce qui est pire que de ne pas l'avoir.
|
||||||
**Corollaire à retenir** : un port qu'on **subit** (le défaut amont d'un logiciel) doit être
|
**Corollaire à retenir** : un port qu'on **subit** (le défaut amont d'un logiciel) doit être
|
||||||
imposé et déclaré comme les autres. Celui d'Alloy ne l'était pas, et c'est la seule raison
|
imposé et déclaré comme les autres. Celui d'Alloy ne l'était pas, et c'est la seule raison
|
||||||
pour laquelle la collision a pu durer des semaines.
|
pour laquelle la collision a pu durer des semaines.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,8 @@
|
||||||
| `serveur_nextcloud` | egress | 443 | tcp | edge | tls-requis | Découverte OIDC auprès de Keycloak (via son FQDN publié à l'edge). |
|
| `serveur_nextcloud` | egress | 443 | tcp | edge | tls-requis | Découverte OIDC auprès de Keycloak (via son FQDN publié à l'edge). |
|
||||||
| `serveur_nextcloud` | egress | 5432 | tcp | serveur_postgresql | tls-requis | Base de données Nextcloud (verify-full). |
|
| `serveur_nextcloud` | egress | 5432 | tcp | serveur_postgresql | tls-requis | Base de données Nextcloud (verify-full). |
|
||||||
| `serveur_nextcloud` | egress | 9980 | tcp | serveur_collabora | tls-cible | Vérifications WOPI serveur->Collabora (édition en ligne). TLS interne = feuille de route edge->backends. |
|
| `serveur_nextcloud` | egress | 9980 | tcp | serveur_collabora | tls-cible | Vérifications WOPI serveur->Collabora (édition en ligne). TLS interne = feuille de route edge->backends. |
|
||||||
| `serveur_nginx` | ingress | 80 | tcp | admin | clair | HTTP depuis le reseau d'administration — redirection permanente vers HTTPS. |
|
| `serveur_nginx` | ingress | 80 | tcp | externe | clair | HTTP entrant — redirection permanente vers HTTPS. |
|
||||||
|
| `serveur_nginx` | ingress | 443 | tcp | externe | tls-requis | HTTPS entrant — services exposés (terminaison TLS à l'edge). |
|
||||||
| `serveur_nginx` | ingress | 443 | tcp | flotte | tls-requis | HTTPS depuis le tenant : les FQDN publiés vivent à l'edge (découverte OIDC, appels inter-services par nom). |
|
| `serveur_nginx` | ingress | 443 | tcp | flotte | tls-requis | HTTPS depuis le tenant : les FQDN publiés vivent à l'edge (découverte OIDC, appels inter-services par nom). |
|
||||||
| `serveur_nginx` | ingress | 443 | tcp | admin | tls-requis | HTTPS depuis le reseau d'administration : l'exploitant administre les services par leur interface web, servie par l'edge. |
|
| `serveur_nginx` | ingress | 443 | tcp | admin | tls-requis | HTTPS depuis le reseau d'administration : l'exploitant administre les services par leur interface web, servie par l'edge. |
|
||||||
| `serveur_nginx` | egress | derive | tcp | expositions | tls-cible | Proxy vers les backends exposés (host:port dérivés des expose ; TLS interne = roadmap edge→backends). |
|
| `serveur_nginx` | egress | derive | tcp | expositions | tls-cible | Proxy vers les backends exposés (host:port dérivés des expose ; TLS interne = roadmap edge→backends). |
|
||||||
|
|
@ -86,14 +87,15 @@
|
||||||
| `serveur_ops` | ingress | 8090 | tcp | edge, admin | clair | Console d'exploitation servie par l'edge (TLS terminé à l'edge), et joignable depuis le plan d'administration là où il n'y a pas d'edge. Le GUI lui-même reste sur la boucle locale : c'est nginx qui authentifie devant. |
|
| `serveur_ops` | ingress | 8090 | tcp | edge, admin | clair | Console d'exploitation servie par l'edge (TLS terminé à l'edge), et joignable depuis le plan d'administration là où il n'y a pas d'edge. Le GUI lui-même reste sur la boucle locale : c'est nginx qui authentifie devant. |
|
||||||
| `serveur_ops` | egress | 22 | tcp | flotte | ssh | Piloter la flotte — c'est la raison d'être du poste. |
|
| `serveur_ops` | egress | 22 | tcp | flotte | ssh | Piloter la flotte — c'est la raison d'être du poste. |
|
||||||
| `serveur_ops` | egress | 443 | tcp | edge | tls-requis | Cloner et resynchroniser le génome depuis la forge de l'écosystème. |
|
| `serveur_ops` | egress | 443 | tcp | edge | tls-requis | Cloner et resynchroniser le génome depuis la forge de l'écosystème. |
|
||||||
|
| `serveur_ops` | egress | 443 | tcp | voisins_site | tls-requis | Cloner le genome depuis la forge du site, quand cet ecosysteme n'heberge pas la sienne. |
|
||||||
|
| `serveur_ops` | egress | 8006 | tcp | externe | tls-requis | API de l'hyperviseur : créer et cloner les VM d'un écosystème descendant. |
|
||||||
| `serveur_ops_site` | egress | 22 | tcp | fabric | ssh | Shell des hyperviseurs : ce que l'API ne couvre pas — configuration reseau, ponts, deplacement de disques. Un pouvoir distinct de l'API, donc declare a part. |
|
| `serveur_ops_site` | egress | 22 | tcp | fabric | ssh | Shell des hyperviseurs : ce que l'API ne couvre pas — configuration reseau, ponts, deplacement de disques. Un pouvoir distinct de l'API, donc declare a part. |
|
||||||
| `serveur_ops_site` | egress | 22 | tcp | serveur_ops_tenant | ssh | Insemination : amorcer le runner d'un tenant — socle, moteur, plan, plancher de resolution — pour qu'il prenne ensuite le relais sur ses propres machines. Ne transporte aucun secret : la voute est remise par un humain. |
|
| `serveur_ops_site` | egress | 22 | tcp | serveur_ops_tenant | ssh | Insemination : amorcer le runner d'un tenant — socle, moteur, plan, plancher de resolution — pour qu'il prenne ensuite le relais sur ses propres machines. Ne transporte aucun secret : la voute est remise par un humain. |
|
||||||
| `serveur_ops_site` | egress | 443 | tcp | fabric | tls-requis | API de la frontiere OPNsense : poser les alias et les regles qui ouvrent les flux du tenant qu'on materialise. Preparer le terrain sans cela laisserait un terrain injoignable. |
|
| `serveur_ops_site` | egress | 443 | tcp | fabric | tls-requis | API de la frontiere OPNsense : poser les alias et les regles qui ouvrent les flux du tenant qu'on materialise. Preparer le terrain sans cela laisserait un terrain injoignable. |
|
||||||
| `serveur_ops_site` | egress | 8006 | tcp | fabric | tls-requis | API de l'hyperviseur : créer, cloner et détruire les VM de la fabric. Le seul flux par lequel un écosystème peut en matérialiser un autre. |
|
| `serveur_ops_site` | egress | 8006 | tcp | fabric | tls-requis | API de l'hyperviseur : créer, cloner et détruire les VM de la fabric. Le seul flux par lequel un écosystème peut en matérialiser un autre. |
|
||||||
| `serveur_ops_tenant` | ingress | 22 | tcp | runner_site | ssh | Insémination : le runner du SITE amorce ce runner-ci — socle, moteur, plan, plancher de résolution — jusqu'à ce qu'un humain lui remette sa voûte. Vers cette machine seule, jamais vers le reste de l'écosystème. |
|
| `serveur_ops_tenant` | ingress | 22 | tcp | runner_site | ssh | Insémination : le runner du SITE amorce ce runner-ci — socle, moteur, plan, plancher de résolution — jusqu'à ce qu'un humain lui remette sa voûte. Vers cette machine seule, jamais vers le reste de l'écosystème. |
|
||||||
| `serveur_postfix` | ingress | 25 | tcp | externe, client_smtp | starttls | SMTP entrant : courrier externe (MX) et notifications internes (client_smtp). |
|
| `serveur_postfix` | ingress | 25 | tcp | externe, client_smtp | starttls | SMTP entrant : courrier externe (MX) et notifications internes (client_smtp). |
|
||||||
| `serveur_postfix` | ingress | 465 | tcp | flotte, externe | tls-requis | Soumission authentifiée en TLS direct (submissions, RFC 8314) : clients de courriel des utilisateurs. |
|
| `serveur_postfix` | ingress | 587 | tcp | flotte | starttls | Soumission authentifiée (submission) pour les agents internes qui envoient du courrier. |
|
||||||
| `serveur_postfix` | ingress | 587 | tcp | flotte, externe | starttls | Soumission authentifiée (STARTTLS obligatoire) : agents internes et clients de courriel des utilisateurs. |
|
|
||||||
| `serveur_postfix` | egress | 24 | tcp | serveur_dovecot | tls-requis | Remise finale par LMTP au mailstore (Dovecot), en TLS vérifié. |
|
| `serveur_postfix` | egress | 24 | tcp | serveur_dovecot | tls-requis | Remise finale par LMTP au mailstore (Dovecot), en TLS vérifié. |
|
||||||
| `serveur_postfix` | egress | 25 | tcp | externe | starttls | Relais sortant vers les MX distants (STARTTLS opportuniste). |
|
| `serveur_postfix` | egress | 25 | tcp | externe | starttls | Relais sortant vers les MX distants (STARTTLS opportuniste). |
|
||||||
| `serveur_postfix` | egress | 636 | tcp | serveur_openldap | tls-requis | Tables virtuelles (domaines/alias/boîtes) résolues sur l'annuaire (LDAPS). |
|
| `serveur_postfix` | egress | 636 | tcp | serveur_openldap | tls-requis | Tables virtuelles (domaines/alias/boîtes) résolues sur l'annuaire (LDAPS). |
|
||||||
|
|
@ -120,18 +122,17 @@
|
||||||
| `serveur_rspamd` | ingress | 11332 | tcp | localhost | clair | Protocole milter consommé par Postfix co-localisé (analyse + signature DKIM). Local uniquement. |
|
| `serveur_rspamd` | ingress | 11332 | tcp | localhost | clair | Protocole milter consommé par Postfix co-localisé (analyse + signature DKIM). Local uniquement. |
|
||||||
| `serveur_rspamd` | ingress | 11334 | tcp | localhost | clair | Interface de contrôle rspamd (statistiques, apprentissage) en local. |
|
| `serveur_rspamd` | ingress | 11334 | tcp | localhost | clair | Interface de contrôle rspamd (statistiques, apprentissage) en local. |
|
||||||
| `serveur_step_ca` | ingress | 8443 | tcp | flotte | tls-requis | ACME + API step-ca : chaque nœud (client_pki) émet/renouvelle ses certificats et récupère la racine. |
|
| `serveur_step_ca` | ingress | 8443 | tcp | flotte | tls-requis | ACME + API step-ca : chaque nœud (client_pki) émet/renouvelle ses certificats et récupère la racine. |
|
||||||
| `serveur_web_dorsal` | ingress | 80 | tcp | edge, serveur_web_frontal | clair | Front nginx local des webapps et des sites statiques, relaye par l'edge (noms internes) et par le web frontal (noms publics). |
|
| `serveur_web_dorsal` | ingress | 80 | tcp | edge | clair | Front nginx local des webapps, proxie par l'edge (TLS termine a l'edge). |
|
||||||
| `serveur_web_dorsal` | egress | 443 | tcp | flotte | tls | git clone/pull du depot de chaque app (Forgejo souverain) au deploiement. |
|
| `serveur_web_dorsal` | egress | 443 | tcp | flotte | tls | git clone/pull du depot de chaque app (Forgejo souverain) au deploiement. |
|
||||||
| `serveur_web_frontal` | ingress | 80 | tcp | externe | clair | HTTP public, relaye a travers le WAF — et le defi HTTP d'ACME ; renverra vers HTTPS quand le certificat sera la. |
|
| `serveur_web_frontal` | ingress | 80 | tcp | edge | clair | Contenu statique servi au navigateur via l'edge (TLS termine a l'edge). |
|
||||||
| `serveur_web_frontal` | ingress | 443 | tcp | externe | tls-requis | HTTPS public — les sites et services que le locataire publie sur l'Internet (attend son certificat Let's Encrypt). |
|
| `serveur_web_frontal` | egress | 443 | tcp | flotte | tls | git clone/pull du depot du site (Forgejo souverain) au deploiement. |
|
||||||
| `serveur_web_frontal` | egress | 80 | tcp | serveur_web_dorsal | clair | Relais vers le web dorsal, qui porte les sites et les applications publiques. |
|
|
||||||
|
|
||||||
## Synthèse chiffrement
|
## Synthèse chiffrement
|
||||||
|
|
||||||
- **clair** : 51 flux
|
- **clair** : 50 flux
|
||||||
- **n-a** : 8 flux
|
- **n-a** : 8 flux
|
||||||
- **ssh** : 8 flux
|
- **ssh** : 8 flux
|
||||||
- **starttls** : 6 flux
|
- **starttls** : 6 flux
|
||||||
- **tls** : 10 flux
|
- **tls** : 11 flux
|
||||||
- **tls-cible** : 2 flux
|
- **tls-cible** : 2 flux
|
||||||
- **tls-requis** : 34 flux
|
- **tls-requis** : 35 flux
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,7 @@ Nommer un point ouvert vaut mieux qu'une ligne rassurante sans mécanisme derri
|
||||||
Aucune de ces responsabilités n'est laissée à la parole :
|
Aucune de ces responsabilités n'est laissée à la parole :
|
||||||
|
|
||||||
```
|
```
|
||||||
make prouver le dépôt est-il cohérent avec lui-même (94 preuves, zéro réseau)
|
make prouver le dépôt est-il cohérent avec lui-même (83 preuves, zéro réseau)
|
||||||
make remise-verifier le second temps de la remise est-il fait, ou en retard ? (P80)
|
make remise-verifier le second temps de la remise est-il fait, ou en retard ? (P80)
|
||||||
make certificats-plan ce que le disque porte contre ce que la mémoire sert
|
make certificats-plan ce que le disque porte contre ce que la mémoire sert
|
||||||
make expositions-plan chaque service publié répond-il, et depuis où
|
make expositions-plan chaque service publié répond-il, et depuis où
|
||||||
|
|
|
||||||
|
|
@ -84,20 +84,6 @@ variables:
|
||||||
DANS:
|
DANS:
|
||||||
invite: "Jours avant le second temps"
|
invite: "Jours avant le second temps"
|
||||||
facultatif: true
|
facultatif: true
|
||||||
JEU:
|
|
||||||
invite: "Le jeu d'etat (openldap, postgresql, nextcloud... — voir restauration-etat)"
|
|
||||||
DEPUIS:
|
|
||||||
invite: "Reprendre a cette etape (vide = depuis le debut)"
|
|
||||||
valeurs: [sauvegarder, raser, creer, inseminer, armer, monter, parefeu, bilan]
|
|
||||||
facultatif: true
|
|
||||||
ARMER:
|
|
||||||
invite: "Armer sans marquer la pause"
|
|
||||||
valeurs: [oui]
|
|
||||||
facultatif: true
|
|
||||||
SOURCE:
|
|
||||||
invite: "Comparer a quel instantane (vide = celui d'avant rasage)"
|
|
||||||
valeurs: [candidat]
|
|
||||||
facultatif: true
|
|
||||||
VMID:
|
VMID:
|
||||||
invite: "Identifiant Proxmox de la VM"
|
invite: "Identifiant Proxmox de la VM"
|
||||||
DIALECTE:
|
DIALECTE:
|
||||||
|
|
@ -213,12 +199,6 @@ runbooks:
|
||||||
- cible: site-intrants
|
- cible: site-intrants
|
||||||
nature: mesure
|
nature: mesure
|
||||||
pourquoi: "Ce que ce site expose a ses locataires, derive et non declare deux fois."
|
pourquoi: "Ce que ce site expose a ses locataires, derive et non declare deux fois."
|
||||||
- cible: fiches-site-deposer
|
|
||||||
nature: ecriture
|
|
||||||
pourquoi: >-
|
|
||||||
Deposer chez chaque locataire la fiche que le site lui destine. Son runner n'a pas
|
|
||||||
le depot du site : c'est par elle que son inventaire se genere sans lui. A commiter
|
|
||||||
dans le depot de chaque locataire.
|
|
||||||
- cible: site-verifier
|
- cible: site-verifier
|
||||||
nature: mesure
|
nature: mesure
|
||||||
pourquoi: "Le playbook du site correspond-il encore aux couches declarees ?"
|
pourquoi: "Le playbook du site correspond-il encore aux couches declarees ?"
|
||||||
|
|
@ -228,21 +208,6 @@ runbooks:
|
||||||
- cible: routes-fabric-etat
|
- cible: routes-fabric-etat
|
||||||
nature: mesure
|
nature: mesure
|
||||||
pourquoi: "Une route de zone manquante est invisible jusqu'au premier invite qui la traverse."
|
pourquoi: "Une route de zone manquante est invisible jusqu'au premier invite qui la traverse."
|
||||||
- cible: locataire-creer
|
|
||||||
nature: ecriture
|
|
||||||
variables: [TENANT, PARALLELE]
|
|
||||||
fixes: {CONFIRMER: "true"}
|
|
||||||
pourquoi: >-
|
|
||||||
Cloner les VM d'un locataire NOMME, d'apres la face reseau qu'il publie : le
|
|
||||||
runner du site materialise sans monter son depot. Memes machines, memes
|
|
||||||
parametres que `flotte-creer` sur l'instance montee (P93, P94).
|
|
||||||
- cible: locataire-raser
|
|
||||||
nature: destructif
|
|
||||||
variables: [TENANT, INSTANCE]
|
|
||||||
fixes: {CONFIRMER: "true"}
|
|
||||||
pourquoi: >-
|
|
||||||
Detruire les VM d'un locataire NOMME, d'apres sa face : les memes VMID que le
|
|
||||||
plan derive (P94). Son nom court s'ecrit en toutes lettres, comme pour `raser`.
|
|
||||||
- cible: site-raser
|
- cible: site-raser
|
||||||
nature: destructif
|
nature: destructif
|
||||||
variables: [SITE]
|
variables: [SITE]
|
||||||
|
|
@ -345,12 +310,6 @@ runbooks:
|
||||||
pourquoi: >-
|
pourquoi: >-
|
||||||
Regenerer le registre des flux et les regles nftables depuis les `meta/flux.yml`
|
Regenerer le registre des flux et les regles nftables depuis les `meta/flux.yml`
|
||||||
des roles. Tout ce qui suit en descend.
|
des roles. Tout ce qui suit en descend.
|
||||||
- cible: face-reseau-publier
|
|
||||||
nature: ecriture
|
|
||||||
pourquoi: >-
|
|
||||||
Publier ce que ce locataire demande a son site (`face-reseau.yml`) : ses machines,
|
|
||||||
ses zones, ses flux deja resolus. Le site ne lit plus que ce fichier. Apres `flux`,
|
|
||||||
et a commiter dans le depot du locataire.
|
|
||||||
- cible: devis-proxmox-fw
|
- cible: devis-proxmox-fw
|
||||||
nature: mesure
|
nature: mesure
|
||||||
pourquoi: "Le pare-feu est-ouest intra-locataire, derive du registre des flux."
|
pourquoi: "Le pare-feu est-ouest intra-locataire, derive du registre des flux."
|
||||||
|
|
@ -421,14 +380,6 @@ runbooks:
|
||||||
pourquoi: >-
|
pourquoi: >-
|
||||||
Les VM manquantes, clonees depuis le gabarit dore. VMID, IP et VLAN sont DERIVES
|
Les VM manquantes, clonees depuis le gabarit dore. VMID, IP et VLAN sont DERIVES
|
||||||
du plan : on ne les saisit nulle part.
|
du plan : on ne les saisit nulle part.
|
||||||
- cible: monter-flotte
|
|
||||||
nature: ecriture
|
|
||||||
duree: "long"
|
|
||||||
fixes: {CONFIRMER: "true"}
|
|
||||||
pourquoi: >-
|
|
||||||
Des VM qui viennent de naitre (ou de renaitre) a la flotte recettee : flux, AC et
|
|
||||||
DNS d'abord, tout le reste, puis `valider`. C'est la sequence du runner apres une
|
|
||||||
reconstruction ; chaque proprietaire d'etat y remet celui d'avant.
|
|
||||||
- cible: deployer-tout
|
- cible: deployer-tout
|
||||||
nature: ecriture
|
nature: ecriture
|
||||||
duree: "long"
|
duree: "long"
|
||||||
|
|
@ -507,24 +458,6 @@ runbooks:
|
||||||
La preuve la plus dure du moteur : detruire un ecosysteme et le refaire depuis le
|
La preuve la plus dure du moteur : detruire un ecosysteme et le refaire depuis le
|
||||||
code seul. A ne lancer que sur un chantier — la prod vit ailleurs.
|
code seul. A ne lancer que sur un chantier — la prod vit ailleurs.
|
||||||
etapes:
|
etapes:
|
||||||
- cible: reconstruire-locataire
|
|
||||||
nature: destructif
|
|
||||||
portee: poste
|
|
||||||
duree: "long"
|
|
||||||
variables: [TENANT, DEPUIS, ARMER]
|
|
||||||
fixes: {CONFIRMER: "true"}
|
|
||||||
pourquoi: >-
|
|
||||||
TOUT, d'une commande, depuis le poste : sauvegarder, raser et recreer (runner du
|
|
||||||
site), amorcer, armer, monter (runner du locataire, etat remis), pare-feu, bilan.
|
|
||||||
Arret a la premiere etape en echec ; `DEPUIS` reprend. Les etapes ci-dessous sont
|
|
||||||
les memes, une a une.
|
|
||||||
- cible: sauvegarder-maintenant
|
|
||||||
nature: ecriture
|
|
||||||
pourquoi: >-
|
|
||||||
Deposer l'etat de chaque noeud JUSTE AVANT de raser. La reconstruction remet le
|
|
||||||
dernier instantane anterieur a la naissance des machines : sans ce depot, c'est
|
|
||||||
celui de la nuit, et la journee est perdue. Il est etiquete « avant-raser » et
|
|
||||||
garde jusqu'a la reconstruction suivante : c'est contre lui que jugent les temoins.
|
|
||||||
- cible: raser
|
- cible: raser
|
||||||
nature: destructif
|
nature: destructif
|
||||||
portee: poste
|
portee: poste
|
||||||
|
|
@ -539,47 +472,11 @@ runbooks:
|
||||||
duree: "long"
|
duree: "long"
|
||||||
fixes: {CONFIRMER: "true"}
|
fixes: {CONFIRMER: "true"}
|
||||||
pourquoi: >-
|
pourquoi: >-
|
||||||
Refaire tout depuis zero : les VM, puis le deploiement complet — ou chaque role
|
Refaire tout depuis zero : les VM, puis le deploiement complet. Si le code ne
|
||||||
proprietaire REMET l'etat de l'incarnation precedente (AC, annuaire, bases,
|
suffit pas, c'est ici qu'on l'apprend.
|
||||||
Nextcloud, courriel, DKIM). Si le code ne suffit pas, c'est ici qu'on l'apprend.
|
|
||||||
- cible: restauration-etat
|
|
||||||
nature: mesure
|
|
||||||
pourquoi: >-
|
|
||||||
Ce que chaque jeu est devenu : restaure (et depuis quel instantane), neuf, en
|
|
||||||
place. Un jeu EN ATTENTE bloque la sauvegarde de son noeud — c'est voulu.
|
|
||||||
- cible: temoins-etat
|
|
||||||
nature: mesure
|
|
||||||
variables: [HOTE, SOURCE]
|
|
||||||
pourquoi: >-
|
|
||||||
L'etat vivant est-il celui d'avant le rasage ? Une perte, ou une identite changee
|
|
||||||
(cles de l'AC, DKIM, instanceid de Nextcloud, mot de passe d'une entree de
|
|
||||||
l'annuaire), est un ecart. Le bilan dit ce que les roles ont fait ; les temoins
|
|
||||||
mesurent ce qui est revenu.
|
|
||||||
- cible: restauration-renoncer
|
|
||||||
nature: destructif
|
|
||||||
variables: [HOTE, JEU]
|
|
||||||
fixes: {CONFIRMER: "true"}
|
|
||||||
pourquoi: >-
|
|
||||||
Ecarter l'etat d'avant d'un jeu, sans le remettre. La sauvegarde reprend, et
|
|
||||||
l'ancien etat sortira de la retention : une decision, pas une reparation.
|
|
||||||
- cible: valider
|
- cible: valider
|
||||||
nature: mesure
|
nature: mesure
|
||||||
pourquoi: "Une reconstruction sans recette n'a rien prouve."
|
pourquoi: "Une reconstruction sans recette n'a rien prouve."
|
||||||
- cible: parefeu-verifier-flotte
|
|
||||||
nature: mesure
|
|
||||||
portee: poste
|
|
||||||
variables: [INSTANCE]
|
|
||||||
pourquoi: >-
|
|
||||||
La sonde `connectivite` est-elle saine sur chaque VM ? C'est le prealable : une VM
|
|
||||||
clonee nait sans ses options de pare-feu, et on n'active que sur une flotte saine.
|
|
||||||
- cible: parefeu-activer-flotte
|
|
||||||
nature: ecriture
|
|
||||||
portee: poste
|
|
||||||
variables: [INSTANCE]
|
|
||||||
fixes: {CONFIRMER: "true"}
|
|
||||||
pourquoi: >-
|
|
||||||
Tout activer d'un coup, puis lire la sonde `connectivite` que chaque VM rapporte a
|
|
||||||
la minute : seul ce qui change apres l'activation compte. Environ trois minutes.
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
- id: gabarit
|
- id: gabarit
|
||||||
|
|
|
||||||
|
|
@ -112,64 +112,6 @@ Note : ne s'applique qu'aux Forgejo **gérées par Set-OPS**.
|
||||||
|
|
||||||
## 5. Restaurer — et d'abord : prouver qu'on peut
|
## 5. Restaurer — et d'abord : prouver qu'on peut
|
||||||
|
|
||||||
### 5.0 La reconstruction remet l'état (depuis le 2026-09-30)
|
|
||||||
|
|
||||||
Jusqu'au 2026-09-30, **une reconstruction repartait d'un état neuf** : nouvelle racine
|
|
||||||
d'AC, annuaire vierge (`sysadmin` au mot de passe d'amorçage), bases, Nextcloud et courriel
|
|
||||||
vides. Les instantanés se restauraient pour *prouver* qu'ils s'ouvrent, jamais pour être
|
|
||||||
remis en service.
|
|
||||||
|
|
||||||
Désormais **chaque rôle propriétaire remet l'état de l'incarnation précédente**, au moment
|
|
||||||
où il le créerait neuf — la bonne séquence vient des couches de déploiement :
|
|
||||||
|
|
||||||
| Rôle | Quand | Condition « vierge » |
|
|
||||||
|---|---|---|
|
|
||||||
| `serveur_step_ca` | avant `step ca init` | pas de `ca.json` — et la voûte doit ouvrir les clés restaurées |
|
|
||||||
| `serveur_postgresql` | juste après la création des bases | base sans table (hors `nextcloud`, `icinga`) |
|
|
||||||
| `serveur_openldap` | en fin de rôle (schéma et `ppolicy` chargés) | aucune entrée hors la racine |
|
|
||||||
| `serveur_dovecot` | après le répertoire des boîtes | répertoire vide |
|
|
||||||
| `serveur_rspamd` | avant la génération DKIM | pas de clé DKIM |
|
|
||||||
| `serveur_forgejo` | avant l'arborescence de données | répertoire vide |
|
|
||||||
| `serveur_nextcloud` | **en fin de rôle** : base + fichiers + config ensemble, puis `occ upgrade` | pas de `config.php` au départ, base vide |
|
|
||||||
| `serveur_web_dorsal` | après sa racine | racine vide |
|
|
||||||
|
|
||||||
**L'instantané remis** est le dernier pris **avant la naissance de la machine** (date de
|
|
||||||
sa clé d'hôte SSH). Un état en place n'est jamais écrasé d'office. Chaque jeu laisse un
|
|
||||||
marqueur dans `/etc/setops/restauration/<jeu>` ; ce qui a été remplacé est mis de côté dans
|
|
||||||
`/var/backups/setops-avant-restauration/`.
|
|
||||||
|
|
||||||
**La sauvegarde refuse de déposer** (code 3) tant qu'un état d'avant n'a été ni remis ni
|
|
||||||
écarté : sinon `restic forget --keep-daily` chasserait l'instantané d'avant au profit de
|
|
||||||
l'état neuf du même jour.
|
|
||||||
|
|
||||||
**L'instantané d'avant rasage est étiqueté** `avant-raser` (depuis le 2026-10-07) et la
|
|
||||||
rétention le garde **jusqu'à la reconstruction suivante**, qui lui retire l'étiquette. Sans
|
|
||||||
elle, le premier dépôt de la machine reconstruite le chassait le jour même : après la
|
|
||||||
reconstruction de Technolibre (M4), il ne restait rien à quoi comparer l'état remis.
|
|
||||||
|
|
||||||
**Les témoins** comparent l'état vivant à cet instantané. Un écart, c'est une **perte**
|
|
||||||
(fichier, courriel, entrée de l'annuaire, base, rôle, table, ligne d'historique, ou une
|
|
||||||
base dont plus aucune ligne d'avant ne subsiste) ou une **identité changée** : clés et
|
|
||||||
certificats de l'AC, AC d'Icinga et environnement d'Icinga DB, clé DKIM, `instanceid` de
|
|
||||||
Nextcloud, mot de passe d'une entrée de l'annuaire. PostgreSQL se compare par clé (première
|
|
||||||
colonne de chaque table), pas par nombre de lignes. Le reste (un courriel reçu depuis, une table de sessions, le bayes de rspamd,
|
|
||||||
un cache) est listé sans être un écart. La reconstruction les lance à son étape `bilan`.
|
|
||||||
|
|
||||||
Les gestes :
|
|
||||||
|
|
||||||
```
|
|
||||||
make sauvegarder-maintenant # JUSTE AVANT de raser : étiqueté « avant-raser »
|
|
||||||
make restauration-etat [HOTE=...] # ce que chaque jeu est devenu, et si son instantané est encore au dépôt
|
|
||||||
make temoins-etat [HOTE=...] # l'état vivant contre l'instantané d'avant rasage
|
|
||||||
make temoins-etat SOURCE=candidat # faute d'étiquette : contre le dernier d'avant la naissance
|
|
||||||
make restauration-renoncer HOTE=.. JEU=.. CONFIRMER=true # écarter un état, sans le remettre
|
|
||||||
-e client_backup_restauration_instantane=<id> # imposer un instantané, par hôte
|
|
||||||
```
|
|
||||||
|
|
||||||
Sur un nœud, sans Ansible : `setops-restaurer etat`, `setops-restaurer temoins`, et les répétitions qui ne touchent à
|
|
||||||
rien — `setops-restaurer annuaire --essai <base_dn>`, `base <nom> --vers epreuve`,
|
|
||||||
`fichiers --vers /var/tmp/epreuve <chemin>`.
|
|
||||||
|
|
||||||
> Éprouvé le 2026-08-12 sur Chezlepro. `make valider` rejoue la partie automatisable ;
|
> Éprouvé le 2026-08-12 sur Chezlepro. `make valider` rejoue la partie automatisable ;
|
||||||
> la restauration d'une base reste manuelle, et porte un piège décrit plus bas.
|
> la restauration d'une base reste manuelle, et porte un piège décrit plus bas.
|
||||||
|
|
||||||
|
|
@ -201,11 +143,10 @@ la base badger de step-ca, qui avance à **chaque** émission de certificat.
|
||||||
|
|
||||||
`pg_dumpall` écrit `CREATE DATABASE <suivante>` **avant** le `\connect` correspondant.
|
`pg_dumpall` écrit `CREATE DATABASE <suivante>` **avant** le `\connect` correspondant.
|
||||||
Découper « du `\connect X` au `\connect` suivant » emporte donc un ordre qui vise une
|
Découper « du `\connect X` au `\connect` suivant » emporte donc un ordre qui vise une
|
||||||
**autre** base. Couper aussi sur `CREATE DATABASE` et sur `DROP DATABASE` (avec `--clean`,
|
**autre** base. Couper aussi sur `CREATE DATABASE`, et **vérifier avant de rejouer** :
|
||||||
la section de `nextcloud` finissait par `DROP DATABASE postgres;` — 2026-09-30), et **vérifier avant de rejouer** :
|
|
||||||
|
|
||||||
```
|
```
|
||||||
awk '/^\\connect forgejo$/{f=1;next} f && (/^\\connect /||/^CREATE DATABASE /||/^DROP DATABASE /){exit} f' \
|
awk '/^\\connect forgejo$/{f=1;next} f && (/^\\connect /||/^CREATE DATABASE /){exit} f' \
|
||||||
toutes-bases.sql > section.sql
|
toutes-bases.sql > section.sql
|
||||||
|
|
||||||
grep -qE '^(DROP|CREATE|ALTER) DATABASE|^\\connect' section.sql \
|
grep -qE '^(DROP|CREATE|ALTER) DATABASE|^\\connect' section.sql \
|
||||||
|
|
|
||||||
|
|
@ -15,10 +15,7 @@ ce poste et sur le runner de son écosystème, qui en reçoit une copie chiffré
|
||||||
d'une seconde couche ; le script refuse tout fichier dont l'en-tête n'est pas
|
d'une seconde couche ; le script refuse tout fichier dont l'en-tête n'est pas
|
||||||
`$ANSIBLE_VAULT`, garde le chemin de chaque voûte (elles s'appellent presque toutes
|
`$ANSIBLE_VAULT`, garde le chemin de chaque voûte (elles s'appellent presque toutes
|
||||||
`vault.yml`) et relit l'archive empreinte par empreinte. Restauration :
|
`vault.yml`) et relit l'archive empreinte par empreinte. Restauration :
|
||||||
`tar -xf "$(ls -t setops-voutes-*.tar | head -1)" -C <dossier des dépôts>` (la plus récente).
|
`tar -xf setops-voutes-*.tar -C <dossier des dépôts>`. **À refaire après toute écriture
|
||||||
Chaque export porte sa date (`setops-voutes-<date>-<poste>.tar`, l'heure en plus au second
|
|
||||||
export du jour) et n'écrit rien si les voûtes n'ont pas changé depuis la dernière archive.
|
|
||||||
**À refaire après toute écriture
|
|
||||||
dans une voûte**, comme les clés après toute voûte nouvelle.
|
dans une voûte**, comme les clés après toute voûte nouvelle.
|
||||||
|
|
||||||
> **Ce chiffre était trois, et il a baissé sans que rien ne le signale.** Un coffre répliqué deux fois reste
|
> **Ce chiffre était trois, et il a baissé sans que rien ne le signale.** Un coffre répliqué deux fois reste
|
||||||
|
|
@ -80,7 +77,7 @@ Une procédure rangée dans le dépôt serait donc inaccessible exactement quand
|
||||||
La clé se suffit donc à elle-même. À côté de l'archive :
|
La clé se suffit donc à elle-même. À côté de l'archive :
|
||||||
|
|
||||||
```
|
```
|
||||||
setops-cles-<date>-<poste>.tar.gpg les clés, chiffrées (une archive datée par export)
|
setops-cles-<poste>.tar.gpg les clés, chiffrées
|
||||||
restaurer_cles.py le script, autonome
|
restaurer_cles.py le script, autonome
|
||||||
LISEZ-MOI-RESTAURATION.txt le mode d'emploi, et les commandes manuelles
|
LISEZ-MOI-RESTAURATION.txt le mode d'emploi, et les commandes manuelles
|
||||||
```
|
```
|
||||||
|
|
@ -92,9 +89,8 @@ LISEZ-MOI-RESTAURATION.txt le mode d'emploi, et les commandes manuelles
|
||||||
Sur la machine neuve, il ne faut que `gpg`, `python3` et la phrase de passe :
|
Sur la machine neuve, il ne faut que `gpg`, `python3` et la phrase de passe :
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
A="$(ls -t setops-cles-*.tar* | head -1)" # la plus récente
|
python3 restaurer_cles.py setops-cles-*.tar.gpg --lister # voir sans rien écrire
|
||||||
python3 restaurer_cles.py "$A" --lister # voir sans rien écrire
|
python3 restaurer_cles.py setops-cles-*.tar.gpg # remettre en place
|
||||||
python3 restaurer_cles.py "$A" # remettre en place
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Le script remet chaque fichier à sa place selon son nom, et **repose les droits à 0600**.
|
Le script remet chaque fichier à sa place selon son nom, et **repose les droits à 0600**.
|
||||||
|
|
|
||||||
|
|
@ -32,8 +32,7 @@ vault_postgresql_keycloak: ""
|
||||||
vault_bd_keycloak: ""
|
vault_bd_keycloak: ""
|
||||||
vault_bd_forgejo: ""
|
vault_bd_forgejo: ""
|
||||||
vault_bd_icingadb: ""
|
vault_bd_icingadb: ""
|
||||||
# La base de la console (vigie) dans tous les modes : comptes en mode `db`, préférences
|
# La base des COMPTES de la console (mode `db`) — distincte de celle du moteur.
|
||||||
# et migrations partout — distincte de celle du moteur.
|
|
||||||
vault_bd_icingaweb2: ""
|
vault_bd_icingaweb2: ""
|
||||||
|
|
||||||
# --- Forge (Forgejo) ---
|
# --- Forge (Forgejo) ---
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ depots:
|
||||||
composant: main
|
composant: main
|
||||||
# Sur CHAQUE machine, via `client_pki`. Le paquet le plus critique de la liste.
|
# Sur CHAQUE machine, via `client_pki`. Le paquet le plus critique de la liste.
|
||||||
paquets:
|
paquets:
|
||||||
step-cli: 0.31.0-1
|
step-cli: 0.30.6-1
|
||||||
|
|
||||||
icinga:
|
icinga:
|
||||||
uri: https://packages.icinga.com/debian
|
uri: https://packages.icinga.com/debian
|
||||||
|
|
@ -37,12 +37,12 @@ depots:
|
||||||
icinga-archive-keyring: 2.0.0-1+debian13
|
icinga-archive-keyring: 2.0.0-1+debian13
|
||||||
icingacli: 2.14.0-2+debian13
|
icingacli: 2.14.0-2+debian13
|
||||||
icingadb: 1.5.1-8+debian13
|
icingadb: 1.5.1-8+debian13
|
||||||
icingadb-redis: 8.2.10-1+debian13
|
icingadb-redis: 8.2.9-1+debian13
|
||||||
icingadb-web: 1.4.0-1+debian13
|
icingadb-web: 1.4.0-1+debian13
|
||||||
icinga-l10n: 1.4.0-1+debian13
|
icinga-l10n: 1.4.0-1+debian13
|
||||||
icinga-php-legacy: 1.1.0-1+debian13
|
icinga-php-legacy: 1.1.0-1+debian13
|
||||||
icinga-php-library: 1.0.1-1+debian13
|
icinga-php-library: 1.0.0-2+debian13
|
||||||
icinga-php-thirdparty: 1.0.1-1+debian13
|
icinga-php-thirdparty: 1.0.0-2+debian13
|
||||||
icingaweb2: 2.14.0-2+debian13
|
icingaweb2: 2.14.0-2+debian13
|
||||||
icingaweb2-common: 2.14.0-2+debian13
|
icingaweb2-common: 2.14.0-2+debian13
|
||||||
icingaweb2-module-monitoring: 2.12.6-1+debian13
|
icingaweb2-module-monitoring: 2.12.6-1+debian13
|
||||||
|
|
@ -53,12 +53,7 @@ depots:
|
||||||
suite: stable
|
suite: stable
|
||||||
composant: main
|
composant: main
|
||||||
# `alloy` est universel comme `step-cli` : `client_metrique` le pose partout.
|
# `alloy` est universel comme `step-cli` : `client_metrique` le pose partout.
|
||||||
# RELEVES LE 2026-10-03 sur ce que portent les locataires depuis leur reconstruction du
|
|
||||||
# 2026-10-01 : leurs runners n'ont pas ce cache et avaient tire la derniere version
|
|
||||||
# publiee. Le cache du poste, lui, servait encore les anciennes — un deploiement depuis
|
|
||||||
# le poste aurait tente de les RETROGRADER (`paquets_tiers` installe les .deb du cache).
|
|
||||||
# Le site, encore en 1.19.2 / 13.2.1 / 3.7.7, montera a son prochain passage.
|
|
||||||
paquets:
|
paquets:
|
||||||
alloy: 1.20.1-1
|
alloy: 1.19.2-1
|
||||||
grafana: 13.2.3
|
grafana: 13.2.1
|
||||||
loki: 3.7.8
|
loki: 3.7.7
|
||||||
|
|
|
||||||
|
|
@ -1,121 +0,0 @@
|
||||||
---
|
|
||||||
# L'ETAT D'UNE INCARNATION PRECEDENTE — voir roles/client_backup/templates/restaurer.sh.j2
|
|
||||||
#
|
|
||||||
# make restauration-etat [HOTE=...]
|
|
||||||
# ce qui attend dans le depot, et ce que chaque jeu est devenu (lecture seule) ;
|
|
||||||
# make sauvegarder-maintenant [HOTE=...]
|
|
||||||
# deposer TOUT DE SUITE, et attendre que ce soit fait. A faire juste avant `raser` :
|
|
||||||
# sinon la reconstruction remet l'etat de la derniere nuit, et perd la journee ;
|
|
||||||
# make temoins-etat [HOTE=...] [SOURCE=candidat]
|
|
||||||
# l'etat VIVANT est-il celui d'avant le rasage ? Compare a l'instantane etiquete
|
|
||||||
# « avant-raser » (lecture seule). Une perte, ou une identite changee (cles de l'AC,
|
|
||||||
# DKIM, instanceid, mot de passe d'une entree de l'annuaire), fait echouer le jeu ;
|
|
||||||
# make restauration-renoncer HOTE=<hote> JEU=<jeu> CONFIRMER=true
|
|
||||||
# ECARTER un etat anterieur sans le remettre. La sauvegarde du noeud, qui refusait
|
|
||||||
# de deposer pour ne pas le chasser de la retention, reprend — et l'etat d'avant
|
|
||||||
# finira par sortir de la retention. C'est une decision, pas une reparation.
|
|
||||||
#
|
|
||||||
# La REMISE, elle, n'a pas de cible : elle est faite par le role proprietaire de l'etat,
|
|
||||||
# au deploiement, au moment ou il le creerait neuf.
|
|
||||||
- name: L'etat d'une incarnation precedente
|
|
||||||
hosts: client_backup
|
|
||||||
become: true
|
|
||||||
gather_facts: false
|
|
||||||
vars:
|
|
||||||
restauration_action: etat
|
|
||||||
restauration_jeu: ""
|
|
||||||
restauration_source: avant-raser
|
|
||||||
tasks:
|
|
||||||
- name: Refuser un renoncement sans jeu nomme
|
|
||||||
ansible.builtin.assert:
|
|
||||||
that:
|
|
||||||
- restauration_jeu | length > 0
|
|
||||||
fail_msg: "JEU=<jeu> requis (voir `make restauration-etat` pour les noms)."
|
|
||||||
when: restauration_action == 'renoncer'
|
|
||||||
|
|
||||||
- name: Lire ce qui attend, et ce qui a ete tranche
|
|
||||||
ansible.builtin.command:
|
|
||||||
argv: [/usr/local/sbin/setops-restaurer, etat]
|
|
||||||
register: restauration_lue
|
|
||||||
changed_when: false
|
|
||||||
failed_when: false
|
|
||||||
when: restauration_action == 'etat'
|
|
||||||
|
|
||||||
- name: Etat
|
|
||||||
ansible.builtin.debug:
|
|
||||||
msg: "{{ (restauration_lue.stdout_lines | default([])) + (restauration_lue.stderr_lines | default([])) }}"
|
|
||||||
when: restauration_action == 'etat'
|
|
||||||
|
|
||||||
# L'unite est `oneshot` : `systemctl start` rend la main quand le depot est fait, et
|
|
||||||
# echoue s'il a echoue — y compris quand la garde refuse (etat d'avant non remis).
|
|
||||||
- name: Ce noeud a-t-il quelque chose a deposer ?
|
|
||||||
ansible.builtin.stat:
|
|
||||||
path: /etc/systemd/system/setops-sauvegarde.service
|
|
||||||
register: restauration_unite
|
|
||||||
when: restauration_action == 'sauvegarder'
|
|
||||||
|
|
||||||
# LE DEPOT D'AVANT RASAGE EST ETIQUETE (2026-10-07), et la retention le garde jusqu'a la
|
|
||||||
# reconstruction suivante. Un noeud qui n'a pas encore cette unite deposerait SANS
|
|
||||||
# etiquette, et la premiere sauvegarde d'apres reconstruction le chasserait : c'est
|
|
||||||
# exactement ce qui s'est passe a M4. On refuse plutot que de deposer a moitie.
|
|
||||||
- name: L'unite du depot d'avant rasage est-elle deployee ?
|
|
||||||
ansible.builtin.stat:
|
|
||||||
path: /etc/systemd/system/setops-sauvegarde-avant-raser.service
|
|
||||||
register: restauration_unite_avant_raser
|
|
||||||
when:
|
|
||||||
- restauration_action == 'sauvegarder'
|
|
||||||
- restauration_unite.stat.exists
|
|
||||||
|
|
||||||
- name: Refuser un depot que la retention ne garderait pas
|
|
||||||
ansible.builtin.assert:
|
|
||||||
that:
|
|
||||||
- restauration_unite_avant_raser.stat.exists
|
|
||||||
fail_msg: >-
|
|
||||||
setops-sauvegarde-avant-raser.service absente : redeployer client_backup
|
|
||||||
(make appliquer GROUPE=client_backup) avant de raser.
|
|
||||||
when:
|
|
||||||
- restauration_action == 'sauvegarder'
|
|
||||||
- restauration_unite.stat.exists
|
|
||||||
|
|
||||||
- name: Deposer maintenant, etiquete avant rasage
|
|
||||||
ansible.builtin.systemd:
|
|
||||||
name: setops-sauvegarde-avant-raser.service
|
|
||||||
state: started
|
|
||||||
when:
|
|
||||||
- restauration_action == 'sauvegarder'
|
|
||||||
- restauration_unite.stat.exists
|
|
||||||
|
|
||||||
# Code 4 = rien a comparer (aucun etat, ou aucun instantane d'avant rasage) : ce n'est
|
|
||||||
# pas un echec. Code 1 = ecart : on affiche d'abord, on echoue ensuite.
|
|
||||||
- name: Temoins — l'etat vivant contre l'instantane d'avant rasage
|
|
||||||
ansible.builtin.command:
|
|
||||||
argv: >-
|
|
||||||
{{ ['/usr/local/sbin/setops-restaurer', 'temoins']
|
|
||||||
+ (['--candidat'] if restauration_source == 'candidat' else []) }}
|
|
||||||
register: restauration_temoins
|
|
||||||
changed_when: false
|
|
||||||
failed_when: false
|
|
||||||
when: restauration_action == 'temoins'
|
|
||||||
|
|
||||||
- name: Temoins
|
|
||||||
ansible.builtin.debug:
|
|
||||||
msg: "{{ (restauration_temoins.stdout_lines | default([])) + (restauration_temoins.stderr_lines | default([])) }}"
|
|
||||||
when: restauration_action == 'temoins'
|
|
||||||
|
|
||||||
- name: Temoins — verdict
|
|
||||||
ansible.builtin.assert:
|
|
||||||
that:
|
|
||||||
- restauration_temoins.rc in [0, 4]
|
|
||||||
fail_msg: >-
|
|
||||||
{{ 'ECART : une perte, ou une identite changee (voir ci-dessus).'
|
|
||||||
if restauration_temoins.rc == 1 else
|
|
||||||
'setops-restaurer temoins a echoue (code ' ~ restauration_temoins.rc ~ ') : '
|
|
||||||
~ 'client_backup est-il deploye ?' }}
|
|
||||||
quiet: true
|
|
||||||
when: restauration_action == 'temoins'
|
|
||||||
|
|
||||||
- name: Ecarter l'etat anterieur de ce jeu
|
|
||||||
ansible.builtin.command:
|
|
||||||
argv: [/usr/local/sbin/setops-restaurer, acter, "{{ restauration_jeu }}", abandonne]
|
|
||||||
changed_when: true
|
|
||||||
when: restauration_action == 'renoncer'
|
|
||||||
|
|
@ -37,20 +37,3 @@
|
||||||
pre_tasks: *pre_sante
|
pre_tasks: *pre_sante
|
||||||
roles:
|
roles:
|
||||||
- client_sante
|
- client_sante
|
||||||
|
|
||||||
# LE RETRAIT, SUR LES HOTES SORTIS DU GROUPE (2026-10-03). Un role qu'on cesse d'appliquer
|
|
||||||
# laisse son minuteur derriere lui : les hyperviseurs du site ont pousse vers une adresse
|
|
||||||
# perimee pendant des semaines (voir `roles/client_sante/tasks/retirer.yml`).
|
|
||||||
#
|
|
||||||
# LE GROUPE EST NOMME, pas deduit (`all:!client_sante`) : un hyperviseur n'est pas une VM
|
|
||||||
# de la flotte, et un play qui le touche doit le dire. Chez un tenant, `hyperviseurs`
|
|
||||||
# n'existe pas et ce play ne trouve personne.
|
|
||||||
- name: Intégration client_sante — retrait sur les hyperviseurs qui n'y sont plus
|
|
||||||
hosts: hyperviseurs:!client_sante
|
|
||||||
become: true
|
|
||||||
gather_facts: false
|
|
||||||
tasks:
|
|
||||||
- name: Retirer le porteur de santé
|
|
||||||
ansible.builtin.include_role:
|
|
||||||
name: client_sante
|
|
||||||
tasks_from: retirer.yml
|
|
||||||
|
|
|
||||||
|
|
@ -160,7 +160,6 @@
|
||||||
loop: "{{ genome_a_pousser }}"
|
loop: "{{ genome_a_pousser }}"
|
||||||
loop_control:
|
loop_control:
|
||||||
label: "{{ item.dest }}"
|
label: "{{ item.dest }}"
|
||||||
register: genome_emballe
|
|
||||||
changed_when: true
|
changed_when: true
|
||||||
|
|
||||||
- name: Porter les colis jusqu'au runner
|
- name: Porter les colis jusqu'au runner
|
||||||
|
|
@ -256,45 +255,6 @@
|
||||||
label: "{{ item.depot }}"
|
label: "{{ item.depot }}"
|
||||||
index_var: idx
|
index_var: idx
|
||||||
|
|
||||||
# LES RELEASES AUSSI (2026-10-07). Le colis ne portait que la branche : `v2026.08.21` et
|
|
||||||
# `v2026.10.07` manquaient a la forge du site. Il porte desormais les etiquettes ; on
|
|
||||||
# demande a la forge si elle a TOUTES celles du poste. 404 = aucune etiquette sur ce
|
|
||||||
# depot, ce qui est juste quand le poste n'en a pas non plus.
|
|
||||||
- name: Demander à la forge les étiquettes qu'elle porte
|
|
||||||
ansible.builtin.uri:
|
|
||||||
url: >-
|
|
||||||
https://forge.{{ domaine_interne }}/api/v1/repos/{{ serveur_ops_forge_organisation
|
|
||||||
| default('genome') }}/{{ item.depot }}/git/refs/tags
|
|
||||||
validate_certs: false
|
|
||||||
status_code: [200, 404]
|
|
||||||
loop: "{{ genome_a_pousser }}"
|
|
||||||
loop_control:
|
|
||||||
label: "{{ item.depot }}"
|
|
||||||
register: genome_forge_etiquettes
|
|
||||||
retries: 4
|
|
||||||
delay: 3
|
|
||||||
until: genome_forge_etiquettes is succeeded
|
|
||||||
changed_when: false
|
|
||||||
|
|
||||||
- name: La forge porte-t-elle toutes les releases du poste ?
|
|
||||||
vars:
|
|
||||||
genome_etiquettes_poste: "{{ (genome_emballe.results[idx].stdout | from_json).etiquettes | default([]) }}"
|
|
||||||
genome_etiquettes_forge: >-
|
|
||||||
{{ (genome_forge_etiquettes.results[idx].json | default([]) if genome_forge_etiquettes.results[idx].status == 200 else [])
|
|
||||||
| map(attribute='ref') | map('regex_replace', '^refs/tags/', '') | list }}
|
|
||||||
ansible.builtin.assert:
|
|
||||||
that:
|
|
||||||
- genome_etiquettes_poste | difference(genome_etiquettes_forge) | length == 0
|
|
||||||
fail_msg: >-
|
|
||||||
{{ item.depot }} : la forge n'a pas
|
|
||||||
{{ genome_etiquettes_poste | difference(genome_etiquettes_forge) | join(', ') }}.
|
|
||||||
success_msg: >-
|
|
||||||
{{ item.depot }} : {{ genome_etiquettes_poste | length }} etiquette(s) du poste, toutes sur la forge.
|
|
||||||
loop: "{{ genome_a_pousser }}"
|
|
||||||
loop_control:
|
|
||||||
label: "{{ item.depot }}"
|
|
||||||
index_var: idx
|
|
||||||
|
|
||||||
# LE RUNNER DU SITE AVANCE SON PROPRE CLONE, ET SA CONSOLE NE LE SAIT PAS (2026-09-20).
|
# LE RUNNER DU SITE AVANCE SON PROPRE CLONE, ET SA CONSOLE NE LE SAIT PAS (2026-09-20).
|
||||||
#
|
#
|
||||||
# C'est LUI qui pousse, donc il doit d'abord recevoir : ce playbook fait avancer le
|
# C'est LUI qui pousse, donc il doit d'abord recevoir : ce playbook fait avancer le
|
||||||
|
|
|
||||||
|
|
@ -11,23 +11,11 @@
|
||||||
gather_facts: false
|
gather_facts: false
|
||||||
become: false
|
become: false
|
||||||
tasks:
|
tasks:
|
||||||
# UN PROMETHEUS QUI VIENT DE REDEMARRER N'A PAS ENCORE RELEVE SES CIBLES (2026-10-08).
|
- name: Interroger l'API des cibles actives
|
||||||
# Depuis qu'obs-01 est sauvegarde, son premier depot (copie a froid, en fin de
|
|
||||||
# deploiement) relance Prometheus : a la reconstruction de Technolibre, la recette l'a
|
|
||||||
# interroge 18 s apres, et deux cibles n'avaient pas encore de releve reussi — saines
|
|
||||||
# quelques minutes plus tard. On redemande donc, une minute au plus, avant de juger :
|
|
||||||
# une vraie panne echoue toujours, une minute plus tard.
|
|
||||||
- name: Interroger l'API des cibles actives (jusqu'a ce que toutes soient relevees, une minute au plus)
|
|
||||||
ansible.builtin.uri:
|
ansible.builtin.uri:
|
||||||
url: "http://localhost:9090/api/v1/targets?state=active"
|
url: "http://localhost:9090/api/v1/targets?state=active"
|
||||||
return_content: true
|
return_content: true
|
||||||
register: prom_targets
|
register: prom_targets
|
||||||
until: >-
|
|
||||||
((prom_targets.content | default('{}') | from_json).get('data', {}).get('activeTargets', [])
|
|
||||||
| rejectattr('health', 'equalto', 'up') | list | length == 0)
|
|
||||||
or (prom_targets.attempts | default(0) | int >= 6)
|
|
||||||
retries: 6
|
|
||||||
delay: 10
|
|
||||||
|
|
||||||
- name: Analyser up / down
|
- name: Analyser up / down
|
||||||
vars:
|
vars:
|
||||||
|
|
|
||||||
|
|
@ -4,15 +4,3 @@
|
||||||
name: auditd
|
name: auditd
|
||||||
state: restarted
|
state: restarted
|
||||||
failed_when: false
|
failed_when: false
|
||||||
|
|
||||||
# REDEMARRER `auditd` NE RECHARGE PAS LES REGLES (mesure du 2026-10-03, Debian 13).
|
|
||||||
#
|
|
||||||
# Elles sont chargees par `audit-rules.service` (`augenrules --load`), une unite a part
|
|
||||||
# dont `auditd` depend — et qu'un redemarrage d'`auditd` ne rejoue pas. Le handler
|
|
||||||
# ci-dessus laissait donc le fichier juste et le noyau sur les regles du dernier demarrage.
|
|
||||||
#
|
|
||||||
# SANS `failed_when: false` : une regle que le noyau refuse doit se voir ici, pas au
|
|
||||||
# prochain redemarrage de la machine.
|
|
||||||
- name: Recharger les regles auditd
|
|
||||||
ansible.builtin.command: augenrules --load
|
|
||||||
changed_when: true
|
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,7 @@
|
||||||
path: "/etc/audit/rules.d/{{ item }}"
|
path: "/etc/audit/rules.d/{{ item }}"
|
||||||
state: absent
|
state: absent
|
||||||
loop: "{{ auditd_fichiers_perimes }}"
|
loop: "{{ auditd_fichiers_perimes }}"
|
||||||
notify:
|
notify: Restart auditd
|
||||||
- Restart auditd
|
|
||||||
- Recharger les regles auditd
|
|
||||||
when: auditd_enabled | bool
|
when: auditd_enabled | bool
|
||||||
|
|
||||||
# LA RETENTION EST UN CHOIX, DONC ELLE S'ECRIT (2026-09-11). Voir defaults/main.yml pour
|
# LA RETENTION EST UN CHOIX, DONC ELLE S'ECRIT (2026-09-11). Voir defaults/main.yml pour
|
||||||
|
|
@ -50,9 +48,7 @@
|
||||||
owner: root
|
owner: root
|
||||||
group: root
|
group: root
|
||||||
mode: "0640"
|
mode: "0640"
|
||||||
notify:
|
notify: Restart auditd
|
||||||
- Restart auditd
|
|
||||||
- Recharger les regles auditd
|
|
||||||
when: auditd_enabled | bool
|
when: auditd_enabled | bool
|
||||||
|
|
||||||
# `failed_when: false` A CACHE LA PANNE PENDANT TOUT UN DEPLOIEMENT (2026-08-30).
|
# `failed_when: false` A CACHE LA PANNE PENDANT TOUT UN DEPLOIEMENT (2026-08-30).
|
||||||
|
|
|
||||||
|
|
@ -36,20 +36,6 @@
|
||||||
-w /etc/step-ca/ -p wa -k pki-autorite
|
-w /etc/step-ca/ -p wa -k pki-autorite
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
# L'EXPORTATEUR DE METRIQUES LIT L'HORLOGE, IL NE LA REGLE PAS (2026-10-03).
|
|
||||||
#
|
|
||||||
# `adjtimex` sert aux deux : regler ET lire. Le collecteur `timex` de `node_exporter` le lit
|
|
||||||
# a chaque collecte, et la regle `time-change` ci-dessous ne peut pas distinguer — le mode
|
|
||||||
# est dans une structure pointee, hors de portee d'un filtre `-F`. Mesure au site : 4
|
|
||||||
# evenements par minute, 35 % de toute la piste d'audit d'une VM, pour un appel en lecture.
|
|
||||||
#
|
|
||||||
# UNE REGLE `never` ETROITE, ET DEVANT : l'audit retient la PREMIERE regle qui correspond.
|
|
||||||
# Elle ne vise que `adjtimex`, que ce binaire-la : `settimeofday`/`clock_settime` restent
|
|
||||||
# audites pour lui aussi, et chrony — qui regle vraiment — reste entierement audite.
|
|
||||||
# Eprouve avant d'ecrire : 4 evenements/min sans elle, 0 avec ; et `auditctl` accepte un
|
|
||||||
# `exe=` absent, donc la ligne ne casse pas le chargement d'une machine sans exportateur
|
|
||||||
# (ce que ferait un `-w` vers un chemin absent, voir plus haut).
|
|
||||||
-a never,exit -F arch=b64 -S adjtimex -F exe=/usr/bin/prometheus-node-exporter
|
|
||||||
-a always,exit -F arch=b64 -S adjtimex,settimeofday,clock_settime -k time-change
|
-a always,exit -F arch=b64 -S adjtimex,settimeofday,clock_settime -k time-change
|
||||||
-a always,exit -F arch=b64 -S sethostname,setdomainname -k system-locale
|
-a always,exit -F arch=b64 -S sethostname,setdomainname -k system-locale
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,6 @@ Sans eux, le rôle refuse de s'exécuter (assertion).
|
||||||
| `client_backup_repo` | `sftp:restic@<cible>:<hôte>` | Un sous-dossier par nœud |
|
| `client_backup_repo` | `sftp:restic@<cible>:<hôte>` | Un sous-dossier par nœud |
|
||||||
| `client_backup_staging` | `/var/backups/setops` | Préparation des dumps |
|
| `client_backup_staging` | `/var/backups/setops` | Préparation des dumps |
|
||||||
| `client_backup_retention` | `--keep-daily 7 --keep-weekly 4 --keep-monthly 6` | `restic forget` |
|
| `client_backup_retention` | `--keep-daily 7 --keep-weekly 4 --keep-monthly 6` | `restic forget` |
|
||||||
| `client_backup_etiquette_avant_raser` | `avant-raser` | étiquette du dépôt d'avant rasage, gardée par la rétention jusqu'à la reconstruction suivante |
|
|
||||||
| `client_backup_horaire` | `*-*-* 02:30:00` | Timer systemd |
|
| `client_backup_horaire` | `*-*-* 02:30:00` | Timer systemd |
|
||||||
| `client_backup_jobs` | `[]` | **À déclarer par nœud** |
|
| `client_backup_jobs` | `[]` | **À déclarer par nœud** |
|
||||||
|
|
||||||
|
|
@ -56,11 +55,7 @@ Sans eux, le rôle refuse de s'exécuter (assertion).
|
||||||
vérifier que chaque nœud porteur d'état déclare ses jobs.
|
vérifier que chaque nœud porteur d'état déclare ses jobs.
|
||||||
- Un dépôt neuf est **vide jusqu'à la première exécution** : après une reconstruction
|
- Un dépôt neuf est **vide jusqu'à la première exécution** : après une reconstruction
|
||||||
from-zero, déclencher une première sauvegarde plutôt que d'attendre 02:30.
|
from-zero, déclencher une première sauvegarde plutôt que d'attendre 02:30.
|
||||||
- Restauration : **faite par la reconstruction elle-même** depuis le 2026-09-30 — chaque rôle
|
- Restauration : `restic restore` avec le même mot de passe (cf. `docs/runbooks-exploitation.md`).
|
||||||
propriétaire inclut `tasks/restaurer.yml` et remet l'état de l'incarnation précédente ;
|
|
||||||
l'outil de nœud est `setops-restaurer` (cf. `docs/runbooks-exploitation.md` §5.0).
|
|
||||||
- Témoins : `setops-restaurer temoins` (comparateur `setops-temoins`) confronte l'état vivant
|
|
||||||
à l'instantané d'avant rasage ; `make temoins-etat`, et l'étape `bilan` de la reconstruction.
|
|
||||||
|
|
||||||
## Prérequis
|
## Prérequis
|
||||||
- `serveur_backup` déployé, et sa `serveur_backup_pubkey` correspondant à la clé privée de
|
- `serveur_backup` déployé, et sa `serveur_backup_pubkey` correspondant à la clé privée de
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,6 @@
|
||||||
# Chaque nœud déclare ses "jobs" ; restic chiffre côté client + applique la rétention.
|
# Chaque nœud déclare ses "jobs" ; restic chiffre côté client + applique la rétention.
|
||||||
client_backup_paquets:
|
client_backup_paquets:
|
||||||
- restic
|
- restic
|
||||||
# `setops-restaurer` remet les repertoires par `rsync --delete` (absent du gabarit).
|
|
||||||
- rsync
|
|
||||||
|
|
||||||
# Cible (nœud serveur_backup) + transport SSH.
|
# Cible (nœud serveur_backup) + transport SSH.
|
||||||
client_backup_cible: "backup-01.{{ domaine_interne }}"
|
client_backup_cible: "backup-01.{{ domaine_interne }}"
|
||||||
|
|
@ -20,10 +18,6 @@ client_backup_staging: "/var/backups/setops"
|
||||||
|
|
||||||
# Rétention (restic forget).
|
# Rétention (restic forget).
|
||||||
client_backup_retention: "--keep-daily 7 --keep-weekly 4 --keep-monthly 6"
|
client_backup_retention: "--keep-daily 7 --keep-weekly 4 --keep-monthly 6"
|
||||||
# L'instantane d'avant rasage (`make sauvegarder-maintenant`) porte cette etiquette, et la
|
|
||||||
# retention la garde EN PLUS de la regle ci-dessus : jusqu'a la reconstruction suivante
|
|
||||||
# (decision de l'exploitant, 2026-10-07). Voir `templates/sauvegarder.sh.j2`.
|
|
||||||
client_backup_etiquette_avant_raser: "avant-raser"
|
|
||||||
|
|
||||||
# Planification (timer systemd).
|
# Planification (timer systemd).
|
||||||
client_backup_horaire: "*-*-* 02:30:00"
|
client_backup_horaire: "*-*-* 02:30:00"
|
||||||
|
|
@ -80,42 +74,9 @@ client_backup_catalogue:
|
||||||
# qu'il a appris. Les regles, elles, se redeploient et ne sont pas ici.
|
# qu'il a appris. Les regles, elles, se redeploient et ne sont pas ici.
|
||||||
nom: rspamd
|
nom: rspamd
|
||||||
chemins: ["/var/lib/rspamd"]
|
chemins: ["/var/lib/rspamd"]
|
||||||
# PAS DE `serveur_web_frontal` (retire le 2026-09-30) : le frontal RELAIE, il ne sert
|
serveur_web_frontal:
|
||||||
# rien lui-meme — ses vhosts, son WAF et sa page 404 se redeploient depuis le depot.
|
nom: web_frontal
|
||||||
# `/srv/web` venait de l'epoque ou il servait du statique ; ses instantanes etaient vides.
|
chemins: ["{{ serveur_web_frontal_racine_base | default('/srv/web') }}"]
|
||||||
# LES METRIQUES ET LES JOURNAUX (2026-10-07). Ils repartaient de zero a chaque
|
|
||||||
# reconstruction. Ils s'ecrivent en continu : une copie A FROID (service arrete le temps
|
|
||||||
# de copier, `setops-copie-a-froid`) les pose dans le repertoire de preparation, et c'est
|
|
||||||
# cette copie que restic emporte. `vivant` dit ou vit la base, pour la restauration et les
|
|
||||||
# temoins, qui la comparent a l'instantane.
|
|
||||||
serveur_prometheus:
|
|
||||||
nom: prometheus
|
|
||||||
vivant: "{{ serveur_prometheus_donnees | default('/var/lib/prometheus/metrics2') }}"
|
|
||||||
commande: >-
|
|
||||||
/usr/local/sbin/setops-copie-a-froid
|
|
||||||
{{ serveur_prometheus_service | default('prometheus') }}
|
|
||||||
{{ serveur_prometheus_donnees | default('/var/lib/prometheus/metrics2') }}
|
|
||||||
{{ client_backup_staging }}/prometheus/metrics2 --blocs-immuables
|
|
||||||
chemins: ["{{ client_backup_staging }}/prometheus"]
|
|
||||||
serveur_loki:
|
|
||||||
nom: loki
|
|
||||||
vivant: "{{ serveur_loki_chemin | default('/var/lib/loki') }}"
|
|
||||||
commande: >-
|
|
||||||
/usr/local/sbin/setops-copie-a-froid
|
|
||||||
{{ serveur_loki_service | default('loki') }}
|
|
||||||
{{ serveur_loki_chemin | default('/var/lib/loki') }}
|
|
||||||
{{ client_backup_staging }}/loki/donnees --lier chunks --copier chunks/index
|
|
||||||
chemins: ["{{ client_backup_staging }}/loki"]
|
|
||||||
serveur_icinga:
|
|
||||||
# L'AC D'ICINGA FAIT L'IDENTITE DE SA BASE (2026-10-07). Icinga 2 derive l'environnement
|
|
||||||
# d'Icinga DB de la cle publique de son AC, et chaque identifiant d'hote, de service et
|
|
||||||
# d'historique de cet environnement. Recreee a chaque reconstruction, elle rendait
|
|
||||||
# l'historique restaure orphelin : on l'avait donc jete (decision du 2026-09-30, revue
|
|
||||||
# le 2026-10-07 — l'historique, c'est la disponibilite qu'on doit pouvoir prouver).
|
|
||||||
nom: icinga
|
|
||||||
chemins:
|
|
||||||
- "{{ (serveur_icinga_ca | default('/var/lib/icinga2/ca/ca.crt')) | dirname }}"
|
|
||||||
- /var/lib/icinga2/icingadb.env
|
|
||||||
serveur_web_dorsal:
|
serveur_web_dorsal:
|
||||||
nom: web_dorsal
|
nom: web_dorsal
|
||||||
chemins: ["{{ serveur_web_dorsal_racine | default('/srv/webapp') }}"]
|
chemins: ["{{ serveur_web_dorsal_racine | default('/srv/webapp') }}"]
|
||||||
|
|
@ -150,8 +111,6 @@ client_backup_icinga_hote: "{{ (groups['serveur_icinga'] | default([]) | first)
|
||||||
client_backup_icinga_utilisateur: "setops-depot"
|
client_backup_icinga_utilisateur: "setops-depot"
|
||||||
client_backup_icinga_motdepasse: "{{ vault_icinga_api_depot | default('') }}"
|
client_backup_icinga_motdepasse: "{{ vault_icinga_api_depot | default('') }}"
|
||||||
client_backup_ca_verification: "/etc/setops/icinga-ca.crt"
|
client_backup_ca_verification: "/etc/setops/icinga-ca.crt"
|
||||||
# L'Icinga auquel ce noeud a deja rapporte (empreinte de son AC) — voir tasks/verifier.yml.
|
|
||||||
client_backup_marque_icinga: "/etc/setops/client_backup-icinga-entendu"
|
|
||||||
client_backup_icinga_ca_source: "/var/lib/icinga2/ca/ca.crt"
|
client_backup_icinga_ca_source: "/var/lib/icinga2/ca/ca.crt"
|
||||||
|
|
||||||
# `ttl` du resultat passif : au-dela, Icinga perime le service de lui-meme — c'est ce qui
|
# `ttl` du resultat passif : au-dela, Icinga perime le service de lui-meme — c'est ce qui
|
||||||
|
|
@ -169,27 +128,3 @@ client_backup_restauration_part: "10%"
|
||||||
# Huit jours : une semaine entre deux controles, plus une journee de marge. Au-dela sans
|
# Huit jours : une semaine entre deux controles, plus une journee de marge. Au-dela sans
|
||||||
# nouvelle, Icinga passe le service au rouge (voir `serveur_icinga_fraicheur_restauration`).
|
# nouvelle, Icinga passe le service au rouge (voir `serveur_icinga_fraicheur_restauration`).
|
||||||
client_backup_ttl_restauration: 691200
|
client_backup_ttl_restauration: 691200
|
||||||
|
|
||||||
# Combien de temps un script attend un depot verrouille par un autre (`restic --retry-lock`)
|
|
||||||
# avant d'echouer. Le plus long des occupants est le controle de restauration ; il relit
|
|
||||||
# 10 % du depot et le restaure entier.
|
|
||||||
client_backup_attente_verrou: "30m"
|
|
||||||
|
|
||||||
# --- LA RESTAURATION DANS LA RECONSTRUCTION (2026-09-30) ----------------------------
|
|
||||||
#
|
|
||||||
# Jusqu'ici une reconstruction repartait d'un etat NEUF : les instantanes se restauraient
|
|
||||||
# pour PROUVER qu'ils s'ouvrent, jamais pour etre remis en service. Desormais chaque role
|
|
||||||
# proprietaire d'un etat inclut `tasks/restaurer.yml` au moment ou il le creerait neuf, et
|
|
||||||
# remet celui de l'incarnation precedente s'il existe. Voir `templates/restaurer.sh.j2`.
|
|
||||||
#
|
|
||||||
# `false` : ne jamais restaurer (chaque jeu est alors acte « desactive », et la
|
|
||||||
# sauvegarde ne s'en trouve pas bloquee).
|
|
||||||
client_backup_restauration_active: true
|
|
||||||
# Imposer un instantane (son `short_id`) au lieu du dernier anterieur a la naissance.
|
|
||||||
# Par hote, et le temps d'une passe : `-e client_backup_restauration_instantane=abcd1234`.
|
|
||||||
client_backup_restauration_instantane: ""
|
|
||||||
# Ou chaque jeu laisse la trace de ce qui lui est arrive (restaure, neuf, en_place...).
|
|
||||||
client_backup_restauration_marques: "/etc/setops/restauration"
|
|
||||||
# Ce qui etait en place avant d'etre ecrase. Hors des chemins sauvegardes : on ne veut
|
|
||||||
# pas deposer chez l'hebergeur l'etat qu'on vient justement de remplacer.
|
|
||||||
client_backup_restauration_mise_de_cote: "/var/backups/setops-avant-restauration"
|
|
||||||
|
|
|
||||||
|
|
@ -1,84 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
# Géré par Set-OPS (rôle client_backup). Ne pas éditer à la main.
|
|
||||||
#
|
|
||||||
# setops-copie-a-froid SERVICE SOURCE DESTINATION [--blocs-immuables] [--lier REL]... [--copier REL]...
|
|
||||||
#
|
|
||||||
# Copie COHERENTE d'une base qui ecrit en continu (Prometheus, Loki) : le service est
|
|
||||||
# arrete le temps de la copie, puis redemarre — QUOI QU'IL ARRIVE a la copie, et seulement
|
|
||||||
# s'il tournait avant. La destination n'est remplacee qu'une fois la copie complete.
|
|
||||||
#
|
|
||||||
# POURQUOI A FROID (decision de l'exploitant, 2026-10-07 : « on ne prend pas de risque »).
|
|
||||||
# Copier a chaud un WAL qu'on ecrit, c'est sauvegarder un fichier a moitie ecrit. L'autre
|
|
||||||
# voie — ne garder que les blocs fermes — perdait jusqu'aux deux dernieres heures de
|
|
||||||
# metriques a chaque reconstruction. Mesure au site : 2,9 Go de Prometheus se lisent en
|
|
||||||
# 4,2 s, son WAL seul en 0,3 s ; Loki entier en 2 s.
|
|
||||||
#
|
|
||||||
# `--blocs-immuables` : les sous-dossiers nommes par un ULID (26 caracteres, `01…`) sont
|
|
||||||
# les blocs de Prometheus, que rien ne reecrit apres leur fermeture. On les recopie par
|
|
||||||
# LIENS DURS — instantane, sans place — et le reste (WAL, `chunks_head`) par une vraie
|
|
||||||
# copie. Si la destination est sur un autre systeme de fichiers, on copie tout.
|
|
||||||
#
|
|
||||||
# `--lier REL` lie tout un sous-dossier ; `--copier REL` y copie vraiment un sous-dossier qui,
|
|
||||||
# lui, bouge (2026-10-07). Pour Loki : `--lier chunks --copier chunks/index`. Ses morceaux sont
|
|
||||||
# adresses par leur contenu (`debut:fin:somme`, decode sur site-mon-01) et ne bougent jamais ;
|
|
||||||
# mais `chunks/index/` loge aussi `delete_requests.gz`, la base du compacteur, mesuree
|
|
||||||
# reecrite trois semaines apres sa naissance. La lier, c'etait laisser la sauvegarde changer
|
|
||||||
# avec elle. Copier 6 550 morceaux arretait Loki 13 s ; les lier prend 40 ms.
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
service="${1:?service}"
|
|
||||||
source="${2:?source}"
|
|
||||||
dest="${3:?destination}"
|
|
||||||
shift 3
|
|
||||||
blocs="" lier=() copier=()
|
|
||||||
while (( $# )); do
|
|
||||||
case "$1" in
|
|
||||||
--blocs-immuables) blocs=1; shift ;;
|
|
||||||
--lier) lier+=("$2"); shift 2 ;;
|
|
||||||
--copier) copier+=("$2"); shift 2 ;;
|
|
||||||
*) echo "copie a froid : option inconnue : $1" >&2; exit 2 ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
[[ -d "${source}" ]] || { echo "copie a froid : ${source} n'existe pas — rien a copier." >&2; exit 0; }
|
|
||||||
|
|
||||||
tmp="${dest}.en-cours"
|
|
||||||
rm -rf "${tmp}"
|
|
||||||
mkdir -p "$(dirname "${dest}")"
|
|
||||||
|
|
||||||
actif=0
|
|
||||||
if systemctl is-active --quiet "${service}"; then actif=1; fi
|
|
||||||
relancer() { if (( actif )); then systemctl start "${service}"; fi; }
|
|
||||||
|
|
||||||
debut=$(date +%s%N)
|
|
||||||
if (( actif )); then systemctl stop "${service}"; fi
|
|
||||||
trap relancer EXIT
|
|
||||||
|
|
||||||
lie() { cp -al "$1" "$2" 2>/dev/null || { rm -rf "$2"; cp -a "$1" "$2"; }; }
|
|
||||||
mkdir -p "${tmp}"
|
|
||||||
for e in "${source}"/* "${source}"/.[!.]*; do
|
|
||||||
[[ -e "${e}" ]] || continue
|
|
||||||
n="$(basename "${e}")"
|
|
||||||
if [[ -n "${blocs}" && -d "${e}" && ${#n} -eq 26 && "${n}" == 01* ]] \
|
|
||||||
|| [[ " ${lier[*]:-} " == *" ${n} "* ]]; then
|
|
||||||
lie "${e}" "${tmp}/${n}"
|
|
||||||
else
|
|
||||||
cp -a "${e}" "${tmp}/${n}"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
# Ce qui bouge, a l'interieur d'un dossier lie : remplace par une vraie copie.
|
|
||||||
for c in "${copier[@]:+${copier[@]}}"; do
|
|
||||||
if [[ -e "${source}/${c}" ]]; then
|
|
||||||
rm -rf "${tmp:?}/${c}"
|
|
||||||
mkdir -p "$(dirname "${tmp}/${c}")"
|
|
||||||
cp -a "${source}/${c}" "${tmp}/${c}"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
trap - EXIT
|
|
||||||
relancer
|
|
||||||
fin=$(date +%s%N)
|
|
||||||
|
|
||||||
rm -rf "${dest}"
|
|
||||||
mv "${tmp}" "${dest}"
|
|
||||||
echo "copie a froid de ${source} : ${service} $( (( actif )) && echo "arrete (jusqu a la relance, rejeu du WAL non compris) $(( (fin - debut) / 1000000 )) ms" || echo "n'etait pas actif")"
|
|
||||||
|
|
@ -1,432 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
# Géré par Set-OPS (rôle client_backup). Ne pas éditer à la main.
|
|
||||||
"""Témoins d'état : l'état VIVANT de ce noeud est-il celui qu'il avait avant le rasage ?
|
|
||||||
|
|
||||||
POURQUOI (2026-10-07). Le bilan d'une reconstruction dit ce que chaque rôle a FAIT
|
|
||||||
(« restauré depuis tel instantané ») : c'est le système qui juge son propre travail. Le
|
|
||||||
2026-09-30, les empreintes avaient été relevées à la main, avant et après ; rien ne le
|
|
||||||
gardait dans le dépôt. Le 2026-10-07 (M4), l'instantané d'avant le rasage a même disparu
|
|
||||||
du dépôt dans l'heure, chassé par la rétention : il ne restait rien à quoi comparer.
|
|
||||||
|
|
||||||
Ce script compare l'instantané extrait par `setops-restaurer temoins` (sous `--avant`) à ce
|
|
||||||
qui vit sur le noeud. Il ne lit que ; il n'écrit rien.
|
|
||||||
|
|
||||||
VERDICTS, PAR JEU :
|
|
||||||
IDENTIQUE rien ne diffère ;
|
|
||||||
CONFORME rien de perdu, l'identité est intacte ; des changements ordinaires, listés
|
|
||||||
(un courriel reçu depuis, une table de sessions, le bayes de rspamd) ;
|
|
||||||
ECART une PERTE (fichier, entrée, base, table), ou un objet d'IDENTITÉ changé :
|
|
||||||
clés et certificats de l'AC, AC d'Icinga et son environnement, clé DKIM,
|
|
||||||
`instanceid` de Nextcloud, mot de passe d'une entrée de l'annuaire ; une
|
|
||||||
base dont plus aucune ligne d'avant ne subsiste ; une ligne d'historique
|
|
||||||
perdue ; des métriques qui ne couvrent plus l'avant-rasage ; des journaux
|
|
||||||
dont aucun morceau d'avant ne subsiste. Exactement ce que la reconstruction du
|
|
||||||
2026-09-30 avait perdu sans que rien ne le dise.
|
|
||||||
|
|
||||||
Code de sortie : 0 sans écart, 1 avec au moins un écart, 2 erreur d'usage.
|
|
||||||
"""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import base64
|
|
||||||
import collections
|
|
||||||
import hashlib
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
LISTE_MAX = 8 # détails affichés par catégorie ; le compte, lui, est toujours entier
|
|
||||||
|
|
||||||
# Attributs que l'annuaire tient lui-même : ils changent à chaque écriture, ou à chaque
|
|
||||||
# rejeu (`slapadd` les repose), sans que la donnée ait bougé.
|
|
||||||
OPERATIONNELS = {"entrycsn", "modifytimestamp", "modifiersname", "createtimestamp",
|
|
||||||
"creatorsname", "contextcsn"}
|
|
||||||
|
|
||||||
|
|
||||||
class Rapport:
|
|
||||||
def __init__(self, jeu: str) -> None:
|
|
||||||
self.jeu = jeu
|
|
||||||
self.ecarts: list[str] = [] # pertes et identités changées
|
|
||||||
self.changes: list[str] = [] # changements ordinaires
|
|
||||||
self.retires: list[str] = [] # disparitions ordinaires (un cache, une base compactee)
|
|
||||||
self.nouveaux: list[str] = []
|
|
||||||
self.compte = 0 # objets comparés
|
|
||||||
|
|
||||||
def verdict(self) -> str:
|
|
||||||
if self.ecarts:
|
|
||||||
return "ECART"
|
|
||||||
return "CONFORME" if (self.changes or self.retires or self.nouveaux) else "IDENTIQUE"
|
|
||||||
|
|
||||||
def lignes(self, unite: str) -> list[str]:
|
|
||||||
out = [f" {self.jeu:<12} {self.verdict():<10} {self.compte} {unite}"]
|
|
||||||
for titre, liste in (("ECART", self.ecarts), ("modifie", self.changes),
|
|
||||||
("retire", self.retires), ("nouveau", self.nouveaux)):
|
|
||||||
for x in liste[:LISTE_MAX]:
|
|
||||||
out.append(f" {titre:<8} {x}")
|
|
||||||
if len(liste) > LISTE_MAX:
|
|
||||||
out.append(f" {titre:<8} ... et {len(liste) - LISTE_MAX} autre(s)")
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
def empreinte(p: Path) -> str:
|
|
||||||
h = hashlib.sha256()
|
|
||||||
with p.open("rb") as f:
|
|
||||||
for bloc in iter(lambda: f.read(1 << 20), b""):
|
|
||||||
h.update(bloc)
|
|
||||||
return h.hexdigest()
|
|
||||||
|
|
||||||
|
|
||||||
def arbre(racine: Path, normaliser=None) -> dict[str, str]:
|
|
||||||
"""{chemin relatif (normalisé) : sha256} des fichiers réguliers sous `racine`."""
|
|
||||||
if racine.is_file():
|
|
||||||
return {racine.name: empreinte(racine)}
|
|
||||||
out: dict[str, str] = {}
|
|
||||||
if not racine.is_dir():
|
|
||||||
return out
|
|
||||||
for p in sorted(racine.rglob("*")):
|
|
||||||
if p.is_file() and not p.is_symlink():
|
|
||||||
rel = p.relative_to(racine).as_posix()
|
|
||||||
out[normaliser(rel) if normaliser else rel] = empreinte(p)
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
def comparer_arbres(r: Rapport, avant: dict[str, str], vivant: dict[str, str], *,
|
|
||||||
strict, perte_stricte) -> None:
|
|
||||||
"""`strict(rel)` : un changement est un écart ; `perte_stricte(rel)` : une perte aussi."""
|
|
||||||
r.compte += len(avant)
|
|
||||||
for rel, h in sorted(avant.items()):
|
|
||||||
if rel not in vivant:
|
|
||||||
if perte_stricte(rel):
|
|
||||||
r.ecarts.append(f"perdu : {rel}")
|
|
||||||
else:
|
|
||||||
r.retires.append(rel)
|
|
||||||
elif vivant[rel] != h:
|
|
||||||
(r.ecarts if strict(rel) else r.changes).append(f"{'IDENTITE ' if strict(rel) else ''}{rel}")
|
|
||||||
r.nouveaux.extend(sorted(set(vivant) - set(avant)))
|
|
||||||
|
|
||||||
|
|
||||||
def jamais(_rel: str) -> bool:
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def toujours(_rel: str) -> bool:
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
# --- Les jeux de fichiers ------------------------------------------------------------
|
|
||||||
|
|
||||||
def maildir(rel: str) -> str:
|
|
||||||
"""Un message garde son nom de base : `new/` -> `cur/` à la lecture, `:2,S` aux drapeaux."""
|
|
||||||
rel = re.sub(r"(^|/)(new|cur)/", r"\1msg/", rel)
|
|
||||||
return re.sub(r":2,[A-Za-z]*$", "", rel)
|
|
||||||
|
|
||||||
|
|
||||||
def est_message(rel: str) -> bool:
|
|
||||||
return "/msg/" in f"/{rel}"
|
|
||||||
|
|
||||||
|
|
||||||
def jeu_fichiers(jeu: str, chemins: list[str], avant: Path) -> Rapport:
|
|
||||||
r = Rapport(jeu)
|
|
||||||
for c in chemins:
|
|
||||||
a_avant, a_vivant = avant / c.lstrip("/"), Path(c)
|
|
||||||
if jeu == "step_ca":
|
|
||||||
# Les clés et les certificats SONT l'AC : un octet de différence et plus rien
|
|
||||||
# de ce qu'elle a signé ne se vérifie. Sa base (badger) se compacte d'elle-même.
|
|
||||||
identite = lambda rel: rel.startswith(("certs/", "secrets/")) # noqa: E731
|
|
||||||
comparer_arbres(r, arbre(a_avant), arbre(a_vivant), strict=identite,
|
|
||||||
perte_stricte=identite)
|
|
||||||
elif jeu == "icinga":
|
|
||||||
# L'AC d'Icinga et l'environnement qu'elle derive : changes, chaque identifiant
|
|
||||||
# d'Icinga DB change, et tout l'historique restaure devient orphelin.
|
|
||||||
identite = lambda rel: rel in ("ca.crt", "ca.key", "icingadb.env") # noqa: E731
|
|
||||||
comparer_arbres(r, arbre(a_avant), arbre(a_vivant), strict=identite,
|
|
||||||
perte_stricte=identite)
|
|
||||||
elif jeu == "courriel":
|
|
||||||
comparer_arbres(r, arbre(a_avant, maildir), arbre(a_vivant, maildir),
|
|
||||||
strict=jamais, perte_stricte=est_message)
|
|
||||||
elif jeu == "rspamd":
|
|
||||||
# La clé DKIM est l'identité du domaine ; le bayes, lui, apprend tous les jours.
|
|
||||||
dkim = lambda rel: rel.startswith("dkim/") # noqa: E731
|
|
||||||
comparer_arbres(r, arbre(a_avant), arbre(a_vivant), strict=dkim, perte_stricte=dkim)
|
|
||||||
elif jeu == "nextcloud":
|
|
||||||
if c.rstrip("/").endswith("/config"):
|
|
||||||
instance_id(r, a_avant / "config.php", a_vivant / "config.php")
|
|
||||||
comparer_arbres(r, arbre(a_avant), arbre(a_vivant), strict=jamais,
|
|
||||||
perte_stricte=jamais)
|
|
||||||
else:
|
|
||||||
# Les fichiers des PERSONNES ; `appdata_*` est un cache que Nextcloud refait.
|
|
||||||
fichier = lambda rel: re.match(r"^(?!appdata_)[^/]+/files/", rel) is not None # noqa: E731
|
|
||||||
comparer_arbres(r, arbre(a_avant), arbre(a_vivant), strict=jamais,
|
|
||||||
perte_stricte=fichier)
|
|
||||||
else:
|
|
||||||
comparer_arbres(r, arbre(a_avant), arbre(a_vivant), strict=jamais,
|
|
||||||
perte_stricte=toujours)
|
|
||||||
return r
|
|
||||||
|
|
||||||
|
|
||||||
def instance_id(r: Rapport, avant: Path, vivant: Path) -> None:
|
|
||||||
def lire(p: Path) -> str:
|
|
||||||
m = re.search(r"'instanceid'\s*=>\s*'([^']+)'", p.read_text(errors="replace")) if p.is_file() else None
|
|
||||||
return m.group(1) if m else ""
|
|
||||||
a, v = lire(avant), lire(vivant)
|
|
||||||
if a and a != v:
|
|
||||||
r.ecarts.append(f"IDENTITE instanceid : {a} -> {v or '(absent)'}")
|
|
||||||
|
|
||||||
|
|
||||||
# --- Les bases copiees a froid : Prometheus et Loki (2026-10-07) ----------------------
|
|
||||||
#
|
|
||||||
# Leurs FICHIERS ne se comparent pas : Prometheus fusionne ses blocs, Loki compacte son
|
|
||||||
# index, et la retention (15 j, 31 j) en retire chaque jour. On compare ce qui compte.
|
|
||||||
|
|
||||||
def blocs_prometheus(racine: Path) -> list[tuple[int, int]]:
|
|
||||||
out = []
|
|
||||||
for meta in racine.glob("01*/meta.json") if racine.is_dir() else []:
|
|
||||||
try:
|
|
||||||
m = json.loads(meta.read_text())
|
|
||||||
out.append((int(m["minTime"]), int(m["maxTime"])))
|
|
||||||
except (ValueError, KeyError):
|
|
||||||
continue
|
|
||||||
return sorted(out)
|
|
||||||
|
|
||||||
|
|
||||||
def segments_wal(racine: Path) -> list[str]:
|
|
||||||
return sorted(p.name for p in (racine / "wal").glob("[0-9]*")) if (racine / "wal").is_dir() else []
|
|
||||||
|
|
||||||
|
|
||||||
def jeu_prometheus(avant: Path, vivant: Path) -> Rapport:
|
|
||||||
"""Les métriques vivantes couvrent-elles encore l'avant-rasage ?
|
|
||||||
|
|
||||||
Le repère : le DÉBUT du bloc le plus récent de l'instantané — quelques heures avant le
|
|
||||||
rasage. Un bloc vivant doit couvrir cet instant. La rétention ne l'a pas retiré (il a
|
|
||||||
moins de 15 jours) ; une base qui n'a pas été restaurée ne peut pas le couvrir : elle
|
|
||||||
est née après la reconstruction. Une base de moins de deux heures n'a pas encore de
|
|
||||||
bloc : son WAL fait alors le repère, et doit se retrouver dans la base vivante, ou y
|
|
||||||
avoir été fermé en bloc.
|
|
||||||
"""
|
|
||||||
r = Rapport("prometheus")
|
|
||||||
a, v = blocs_prometheus(avant), blocs_prometheus(vivant)
|
|
||||||
r.compte = len(a)
|
|
||||||
if a:
|
|
||||||
t = max(debut for debut, _ in a)
|
|
||||||
if not any(debut <= t < fin for debut, fin in v):
|
|
||||||
r.ecarts.append(f"aucun bloc vivant ne couvre {t} (debut du dernier bloc d'avant) : "
|
|
||||||
f"metriques d'avant non restaurees")
|
|
||||||
if v and min(d for d, _ in v) > min(d for d, _ in a):
|
|
||||||
r.changes.append(f"plus ancienne donnee : {min(d for d, _ in a)} -> {min(d for d, _ in v)} (retention)")
|
|
||||||
else:
|
|
||||||
wal = segments_wal(avant)
|
|
||||||
if wal and not v and wal[0] not in segments_wal(vivant):
|
|
||||||
r.ecarts.append(f"le WAL d'avant ({wal[0]}...) n'est pas dans la base vivante, et elle n'a "
|
|
||||||
f"aucun bloc : metriques d'avant non restaurees")
|
|
||||||
if len(v) != len(a):
|
|
||||||
r.changes.append(f"{len(a)} bloc(s) avant, {len(v)} maintenant (fusions, retention, nouveaux)")
|
|
||||||
return r
|
|
||||||
|
|
||||||
|
|
||||||
def jeu_loki(avant: Path, vivant: Path) -> Rapport:
|
|
||||||
"""Des morceaux de journal d'avant subsistent-ils ? Aucun : non restaurés.
|
|
||||||
|
|
||||||
La rétention (31 j) en retire chaque jour : une perte PARTIELLE est listée. Une perte
|
|
||||||
TOTALE, elle, ne vient que d'une base qui n'a pas été remise.
|
|
||||||
"""
|
|
||||||
r = Rapport("loki")
|
|
||||||
# LES MORCEAUX, PAS L'INDEX (2026-10-08). `chunks/index/` loge l'index TSDB et la base du
|
|
||||||
# compacteur : il les fusionne et les remplace. A la reconstruction de Technolibre, trois
|
|
||||||
# de ces fichiers « d'avant » manquaient, et le temoin les disait retires « (retention ?) »
|
|
||||||
# — aucun journal ne l'etait (Loki rendait ceux d'avant le rasage, minute par minute).
|
|
||||||
morceaux = lambda racine: {rel for rel in arbre(racine / "chunks") if not rel.startswith("index/")} # noqa: E731
|
|
||||||
a = morceaux(avant)
|
|
||||||
v = morceaux(vivant)
|
|
||||||
r.compte = len(a)
|
|
||||||
if a and not (a & v):
|
|
||||||
r.ecarts.append(f"aucun des {len(a)} morceau(x) de journal d'avant ne subsiste : journaux non restaures")
|
|
||||||
elif a - v:
|
|
||||||
r.retires.append(f"{len(a - v)} morceau(x) d'avant retire(s) (retention ?)")
|
|
||||||
if v - a:
|
|
||||||
r.nouveaux.append(f"{len(v - a)} morceau(x) nouveau(x)")
|
|
||||||
return r
|
|
||||||
|
|
||||||
|
|
||||||
# --- L'annuaire ----------------------------------------------------------------------
|
|
||||||
|
|
||||||
def entrees_ldif(texte: str) -> dict[str, dict[str, list[str]]]:
|
|
||||||
"""{dn : {attribut : [valeurs triées]}}, attributs opérationnels exclus."""
|
|
||||||
deplie = re.sub(r"\n ", "", texte.replace("\r\n", "\n"))
|
|
||||||
out: dict[str, dict[str, list[str]]] = {}
|
|
||||||
for bloc in deplie.split("\n\n"):
|
|
||||||
attrs: dict[str, list[str]] = {}
|
|
||||||
dn = ""
|
|
||||||
for ligne in bloc.splitlines():
|
|
||||||
if not ligne or ligne.startswith("#") or ":" not in ligne:
|
|
||||||
continue
|
|
||||||
nom, _, val = ligne.partition(":")
|
|
||||||
if val.startswith(":"):
|
|
||||||
val = base64.b64decode(val[1:].strip()).decode("utf-8", "replace")
|
|
||||||
else:
|
|
||||||
val = val.strip()
|
|
||||||
if nom.lower() == "dn":
|
|
||||||
dn = val.lower()
|
|
||||||
elif nom.lower() not in OPERATIONNELS:
|
|
||||||
attrs.setdefault(nom.lower(), []).append(val)
|
|
||||||
if dn:
|
|
||||||
out[dn] = {k: sorted(v) for k, v in attrs.items()}
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
def jeu_annuaire(avant_ldif: Path, vivant_ldif: Path) -> Rapport:
|
|
||||||
r = Rapport("openldap")
|
|
||||||
if not avant_ldif.is_file():
|
|
||||||
r.ecarts.append(f"l'instantane n'emporte pas d'annuaire ({avant_ldif.name})")
|
|
||||||
return r
|
|
||||||
a = entrees_ldif(avant_ldif.read_text(errors="replace"))
|
|
||||||
v = entrees_ldif(vivant_ldif.read_text(errors="replace")) if vivant_ldif.is_file() else {}
|
|
||||||
r.compte = len(a)
|
|
||||||
for dn, attrs in sorted(a.items()):
|
|
||||||
if dn not in v:
|
|
||||||
r.ecarts.append(f"perdue : {dn}")
|
|
||||||
continue
|
|
||||||
if attrs == v[dn]:
|
|
||||||
continue
|
|
||||||
# Le mot de passe d'une entrée, c'est ce que la reconstruction du 2026-09-30 avait
|
|
||||||
# remis à la valeur d'amorçage sans que rien ne le dise.
|
|
||||||
if attrs.get("userpassword") != v[dn].get("userpassword"):
|
|
||||||
r.ecarts.append(f"IDENTITE userPassword : {dn}")
|
|
||||||
diff = sorted(k for k in set(attrs) | set(v[dn])
|
|
||||||
if k != "userpassword" and attrs.get(k) != v[dn].get(k))
|
|
||||||
if diff:
|
|
||||||
r.changes.append(f"{dn} ({', '.join(diff)})")
|
|
||||||
r.nouveaux.extend(sorted(set(v) - set(a)))
|
|
||||||
return r
|
|
||||||
|
|
||||||
|
|
||||||
# --- PostgreSQL ----------------------------------------------------------------------
|
|
||||||
|
|
||||||
def lire_dump(texte: str) -> tuple[set[str], set[str], dict[tuple[str, str, str], list[str]]]:
|
|
||||||
"""(bases, rôles, {(base, table, 1re colonne) : [valeurs]}) d'après un `pg_dumpall`."""
|
|
||||||
bases = set(re.findall(r"^CREATE DATABASE (\S+) ", texte, re.M))
|
|
||||||
roles = set(re.findall(r"^CREATE ROLE (\S+);", texte, re.M))
|
|
||||||
cles: dict[tuple[str, str, str], list[str]] = {}
|
|
||||||
base, table = "", None
|
|
||||||
for ligne in texte.splitlines():
|
|
||||||
if table is not None:
|
|
||||||
if ligne == "\\.":
|
|
||||||
table = None
|
|
||||||
else:
|
|
||||||
cles[table].append(ligne.split("\t", 1)[0])
|
|
||||||
elif ligne.startswith("\\connect "):
|
|
||||||
base = ligne.split()[-1]
|
|
||||||
else:
|
|
||||||
m = re.match(r"^COPY (\S+) \(([^,)]+)[,)].*FROM stdin;$", ligne)
|
|
||||||
if m:
|
|
||||||
table = (base, m.group(1), m.group(2).strip())
|
|
||||||
cles[table] = []
|
|
||||||
return bases, roles, cles
|
|
||||||
|
|
||||||
|
|
||||||
def psql(base: str, sql: str) -> list[str] | None:
|
|
||||||
r = subprocess.run(["runuser", "-u", "postgres", "--", "psql", "-tAX", "-d", base, "-c", sql],
|
|
||||||
capture_output=True, text=True, env=dict(os.environ, PAGER="cat"))
|
|
||||||
return r.stdout.splitlines() if r.returncode == 0 else None
|
|
||||||
|
|
||||||
|
|
||||||
def jeu_postgresql(dump: Path, interroger=psql) -> Rapport:
|
|
||||||
"""Par CLÉ, pas par nombre de lignes (2026-10-07).
|
|
||||||
|
|
||||||
Compter ne voyait rien : l'historique d'Icinga DB, entièrement remplacé (412 lignes
|
|
||||||
d'avant, toutes perdues ; 380 neuves), passait pour « 412 -> 380 ». On compare donc la
|
|
||||||
première colonne de chaque table, lue au même format que le dump (`COPY ... TO STDOUT`).
|
|
||||||
|
|
||||||
Ce qui est un ÉCART, d'après la mesure sur Chezlepro (avant-raser contre vivant) :
|
|
||||||
- une base dont AUCUNE ligne d'avant ne subsiste, toutes tables confondues : elle n'a
|
|
||||||
pas été restaurée (c'était icingadb, ses 30 tables renouvelées) ;
|
|
||||||
- une ligne perdue dans une table d'HISTORIQUE (`*history*`) : par nature, on y
|
|
||||||
ajoute, on n'y retire pas.
|
|
||||||
Le reste est listé : la découverte de grappe de Keycloak (`jgroups_ping`), ses sessions,
|
|
||||||
une configuration de composant régénérée renouvellent leurs clés dans la vie ordinaire.
|
|
||||||
"""
|
|
||||||
r = Rapport("postgresql")
|
|
||||||
if not dump.is_file():
|
|
||||||
r.ecarts.append(f"l'instantane n'emporte pas de dump ({dump.name})")
|
|
||||||
return r
|
|
||||||
bases, roles, cles = lire_dump(dump.read_text(errors="replace"))
|
|
||||||
vivantes = set(interroger("postgres", "select datname from pg_database") or [])
|
|
||||||
roles_vivants = set(interroger("postgres", "select rolname from pg_roles") or [])
|
|
||||||
for b in sorted(bases - vivantes):
|
|
||||||
r.ecarts.append(f"base perdue : {b}")
|
|
||||||
for x in sorted(roles - roles_vivants):
|
|
||||||
r.ecarts.append(f"role perdu : {x}")
|
|
||||||
par_base: dict[str, list[int]] = collections.defaultdict(lambda: [0, 0])
|
|
||||||
for (b, t, col), avant in sorted(cles.items()):
|
|
||||||
if b not in vivantes or not avant:
|
|
||||||
continue
|
|
||||||
r.compte += 1
|
|
||||||
vivant = interroger(b, f"COPY (select {col} from {t}) TO STDOUT")
|
|
||||||
if vivant is None:
|
|
||||||
r.ecarts.append(f"table perdue : {b}.{t}")
|
|
||||||
continue
|
|
||||||
a, v = set(avant), set(vivant)
|
|
||||||
communes = len(a & v)
|
|
||||||
par_base[b][0] += len(a)
|
|
||||||
par_base[b][1] += communes
|
|
||||||
perdues, nouvelles = len(a) - communes, len(v - a)
|
|
||||||
if perdues and "history" in t:
|
|
||||||
r.ecarts.append(f"historique perdu : {b}.{t} : {perdues} ligne(s) d'avant sur {len(a)}")
|
|
||||||
elif perdues or nouvelles:
|
|
||||||
r.changes.append(f"{b}.{t} : {perdues} retiree(s), {nouvelles} nouvelle(s) (avant {len(a)})")
|
|
||||||
for b, (n, communes) in sorted(par_base.items()):
|
|
||||||
if n and not communes:
|
|
||||||
r.ecarts.insert(0, f"base {b} : aucune des {n} ligne(s) d'avant ne subsiste — non restauree")
|
|
||||||
# `template0` n'est jamais dans un `pg_dumpall` : il n'est pas « nouveau », il est hors jeu.
|
|
||||||
r.nouveaux.extend(f"base {b}" for b in sorted(vivantes - bases - {"postgres", "template0"}))
|
|
||||||
return r
|
|
||||||
|
|
||||||
|
|
||||||
# --- Programme -----------------------------------------------------------------------
|
|
||||||
|
|
||||||
def main(argv: list[str] | None = None, interroger=psql) -> int:
|
|
||||||
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
|
||||||
ap.add_argument("--avant", required=True, type=Path, help="racine où l'instantané est extrait")
|
|
||||||
ap.add_argument("--instantane", required=True)
|
|
||||||
ap.add_argument("--heure", default="")
|
|
||||||
ap.add_argument("--hote", default="")
|
|
||||||
ap.add_argument("--jeu", action="append", default=[], help="NOM=CHEMIN[:CHEMIN...]")
|
|
||||||
ap.add_argument("--vivant", action="append", default=[],
|
|
||||||
help="NOM=CHEMIN : ou vit la base d'un jeu copie a froid")
|
|
||||||
ap.add_argument("--ldif", default="", help="chemin du LDIF dans l'instantané")
|
|
||||||
ap.add_argument("--ldif-vivant", type=Path, default=None)
|
|
||||||
ap.add_argument("--dump", default="", help="chemin du pg_dumpall dans l'instantané")
|
|
||||||
a = ap.parse_args(argv)
|
|
||||||
|
|
||||||
print(f"TEMOINS de {a.hote} : instantane {a.instantane} ({a.heure}) contre l'etat vivant")
|
|
||||||
if not a.jeu:
|
|
||||||
print(" SANS OBJET : ce noeud ne detient aucun etat.")
|
|
||||||
return 0
|
|
||||||
ecart = False
|
|
||||||
vivants = dict(v.partition("=")[::2] for v in a.vivant)
|
|
||||||
for spec in a.jeu:
|
|
||||||
nom, _, chemins = spec.partition("=")
|
|
||||||
# La copie a froid est le SEUL sous-dossier de la preparation du jeu.
|
|
||||||
copie = lambda: next((d for d in sorted((a.avant / chemins.split(":")[0].lstrip("/")).glob("*")) # noqa: E731
|
|
||||||
if d.is_dir()), a.avant / "absent")
|
|
||||||
if nom == "prometheus" and nom in vivants:
|
|
||||||
r, unite = jeu_prometheus(copie(), Path(vivants[nom])), "bloc(s)"
|
|
||||||
elif nom == "loki" and nom in vivants:
|
|
||||||
r, unite = jeu_loki(copie(), Path(vivants[nom])), "morceau(x)"
|
|
||||||
elif nom == "openldap":
|
|
||||||
r, unite = jeu_annuaire(a.avant / a.ldif.lstrip("/"), a.ldif_vivant or Path("/nonexistent")), "entree(s)"
|
|
||||||
elif nom == "postgresql":
|
|
||||||
r, unite = jeu_postgresql(a.avant / a.dump.lstrip("/"), interroger), "table(s)"
|
|
||||||
else:
|
|
||||||
r, unite = jeu_fichiers(nom, [c for c in chemins.split(":") if c], a.avant), "fichier(s)"
|
|
||||||
print("\n".join(r.lignes(unite)))
|
|
||||||
ecart = ecart or bool(r.ecarts)
|
|
||||||
print("ECART : une perte, ou une identite qui a change." if ecart
|
|
||||||
else "Aucun ecart : rien de perdu, l'identite est intacte.")
|
|
||||||
return 1 if ecart else 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit(main())
|
|
||||||
|
|
@ -2,36 +2,3 @@
|
||||||
- name: Recharger systemd
|
- name: Recharger systemd
|
||||||
ansible.builtin.systemd:
|
ansible.builtin.systemd:
|
||||||
daemon_reload: true
|
daemon_reload: true
|
||||||
|
|
||||||
# PREMIER RAPPORT A UN ICINGA QUI NE NOUS A JAMAIS ENTENDUS — voir `tasks/verifier.yml`.
|
|
||||||
# Trois temps, puis la trace, dans cet ordre (les gestionnaires s'executent dans l'ordre de ce fichier) :
|
|
||||||
# deposer, pour que le depot porte un instantane de CETTE incarnation ; verifier le depot ;
|
|
||||||
# verifier qu'il se restaure. Les unites sont `oneshot` : chacune rend la main une fois son
|
|
||||||
# rapport parti, et echoue — bruyamment — si la sauvegarde refuse (etat d'avant non remis)
|
|
||||||
# ou si Icinga ne prend pas le resultat.
|
|
||||||
- name: Premier depot
|
|
||||||
ansible.builtin.systemd:
|
|
||||||
name: setops-sauvegarde.service
|
|
||||||
state: started
|
|
||||||
listen: Premier rapport a Icinga
|
|
||||||
|
|
||||||
- name: Premier controle du depot
|
|
||||||
ansible.builtin.systemd:
|
|
||||||
name: setops-verification-depot.service
|
|
||||||
state: started
|
|
||||||
listen: Premier rapport a Icinga
|
|
||||||
|
|
||||||
- name: Premier controle de restauration
|
|
||||||
ansible.builtin.systemd:
|
|
||||||
name: setops-verification-restauration.service
|
|
||||||
state: started
|
|
||||||
listen: Premier rapport a Icinga
|
|
||||||
|
|
||||||
- name: Retenir l'Icinga qui nous a entendus
|
|
||||||
ansible.builtin.copy:
|
|
||||||
content: "{{ client_backup_icinga_empreinte }}\n"
|
|
||||||
dest: "{{ client_backup_marque_icinga }}"
|
|
||||||
owner: root
|
|
||||||
group: root
|
|
||||||
mode: "0600"
|
|
||||||
listen: Premier rapport a Icinga
|
|
||||||
|
|
|
||||||
|
|
@ -1,78 +0,0 @@
|
||||||
---
|
|
||||||
# ACCES AU DEPOT DE CE NOEUD — partage par la sauvegarde (`main.yml`) et par la
|
|
||||||
# restauration (`restaurer.yml`, incluse par les roles proprietaires de l'etat).
|
|
||||||
#
|
|
||||||
# Une seule copie de ces taches : deux facons de poser une cle finiraient par diverger,
|
|
||||||
# et on ne le decouvrirait que le jour ou l'on restaure.
|
|
||||||
- name: Installer restic
|
|
||||||
ansible.builtin.apt:
|
|
||||||
name: "{{ client_backup_paquets }}"
|
|
||||||
state: present
|
|
||||||
update_cache: true
|
|
||||||
|
|
||||||
- name: Créer le répertoire des secrets Set-OPS
|
|
||||||
ansible.builtin.file:
|
|
||||||
path: /etc/setops
|
|
||||||
state: directory
|
|
||||||
owner: root
|
|
||||||
group: root
|
|
||||||
mode: "0700"
|
|
||||||
|
|
||||||
- name: Déposer la clé SSH privée de sauvegarde
|
|
||||||
ansible.builtin.copy:
|
|
||||||
content: "{{ client_backup_ssh_privkey }}"
|
|
||||||
dest: "{{ client_backup_ssh_key }}"
|
|
||||||
owner: root
|
|
||||||
group: root
|
|
||||||
mode: "0600"
|
|
||||||
no_log: true
|
|
||||||
|
|
||||||
- name: Déposer le mot de passe restic
|
|
||||||
ansible.builtin.copy:
|
|
||||||
content: "{{ client_backup_password }}\n"
|
|
||||||
dest: /etc/setops/restic.pass
|
|
||||||
owner: root
|
|
||||||
group: root
|
|
||||||
mode: "0600"
|
|
||||||
no_log: true
|
|
||||||
|
|
||||||
- name: Assurer /root/.ssh
|
|
||||||
ansible.builtin.file:
|
|
||||||
path: /root/.ssh
|
|
||||||
state: directory
|
|
||||||
owner: root
|
|
||||||
group: root
|
|
||||||
mode: "0700"
|
|
||||||
|
|
||||||
- name: Configurer l'accès SSH à la cible de sauvegarde
|
|
||||||
ansible.builtin.blockinfile:
|
|
||||||
path: /root/.ssh/config
|
|
||||||
create: true
|
|
||||||
owner: root
|
|
||||||
group: root
|
|
||||||
mode: "0600"
|
|
||||||
marker: "# {mark} SETOPS BACKUP {{ client_backup_cible }}"
|
|
||||||
block: |
|
|
||||||
Host {{ client_backup_cible }}
|
|
||||||
User {{ client_backup_utilisateur_distant }}
|
|
||||||
IdentityFile {{ client_backup_ssh_key }}
|
|
||||||
IdentitiesOnly yes
|
|
||||||
StrictHostKeyChecking accept-new
|
|
||||||
|
|
||||||
- name: Déployer l'outil de restauration (setops-restaurer)
|
|
||||||
ansible.builtin.template:
|
|
||||||
src: restaurer.sh.j2
|
|
||||||
dest: /usr/local/sbin/setops-restaurer
|
|
||||||
owner: root
|
|
||||||
group: root
|
|
||||||
mode: "0700"
|
|
||||||
|
|
||||||
# LES TEMOINS (2026-10-07) : `setops-restaurer temoins` extrait l'instantane d'avant rasage,
|
|
||||||
# puis ce comparateur le confronte a l'etat vivant. Lecture seule.
|
|
||||||
- name: Déployer le comparateur des témoins (setops-temoins)
|
|
||||||
ansible.builtin.copy:
|
|
||||||
src: setops-temoins.py
|
|
||||||
dest: /usr/local/sbin/setops-temoins
|
|
||||||
owner: root
|
|
||||||
group: root
|
|
||||||
mode: "0700"
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
---
|
|
||||||
# POSER LE MARQUEUR D'UN JEU — APRES le geste, jamais avant : ecrit d'abord, une
|
|
||||||
# restauration ratee laisserait le noeud convaincu d'avoir remis un etat qu'il n'a pas,
|
|
||||||
# et la sauvegarde recommencerait a deposer par-dessus.
|
|
||||||
#
|
|
||||||
# entree : client_backup_acter_etat (restaure, neuf, en_place, desactive, abandonne)
|
|
||||||
# client_backup_restauration (le jeu, et l'instantane s'il y en a un)
|
|
||||||
- name: Restauration — repertoire des marqueurs
|
|
||||||
ansible.builtin.file:
|
|
||||||
path: "{{ client_backup_restauration_marques }}"
|
|
||||||
state: directory
|
|
||||||
owner: root
|
|
||||||
group: root
|
|
||||||
mode: "0700"
|
|
||||||
when: not ansible_check_mode
|
|
||||||
|
|
||||||
- name: Restauration — acter le jeu
|
|
||||||
ansible.builtin.copy:
|
|
||||||
dest: "{{ client_backup_restauration_marques }}/{{ client_backup_restauration.jeu }}"
|
|
||||||
content: |
|
|
||||||
etat={{ client_backup_acter_etat }}
|
|
||||||
instantane={{ client_backup_restauration.instantane | default('') }}
|
|
||||||
date={{ now(utc=true).isoformat() }}
|
|
||||||
owner: root
|
|
||||||
group: root
|
|
||||||
mode: "0600"
|
|
||||||
when: not ansible_check_mode
|
|
||||||
|
|
||||||
- name: Restauration — retenir l'etat acte
|
|
||||||
ansible.builtin.set_fact:
|
|
||||||
client_backup_restauration: "{{ client_backup_restauration | combine({'etat': client_backup_acter_etat}) }}"
|
|
||||||
|
|
@ -34,10 +34,60 @@
|
||||||
+ (client_backup_jobs | map(attribute='nom') | join(', ')),
|
+ (client_backup_jobs | map(attribute='nom') | join(', ')),
|
||||||
'aucun etat non regenerable — aucune sauvegarde installee') }}
|
'aucun etat non regenerable — aucune sauvegarde installee') }}
|
||||||
|
|
||||||
# L'ACCES AU DEPOT vit a part : les roles proprietaires l'incluent aussi, pour RESTAURER
|
- name: Installer restic
|
||||||
# avant meme que ce role-ci ait tourne (l'AC se restaure dans la couche `pki_racine`).
|
ansible.builtin.apt:
|
||||||
- name: Accès au dépôt (restic, clé, mot de passe, SSH) et outil de restauration
|
name: "{{ client_backup_paquets }}"
|
||||||
ansible.builtin.include_tasks: acces.yml
|
state: present
|
||||||
|
update_cache: true
|
||||||
|
|
||||||
|
- name: Créer le répertoire des secrets Set-OPS
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: /etc/setops
|
||||||
|
state: directory
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: "0700"
|
||||||
|
|
||||||
|
- name: Déposer la clé SSH privée de sauvegarde
|
||||||
|
ansible.builtin.copy:
|
||||||
|
content: "{{ client_backup_ssh_privkey }}"
|
||||||
|
dest: "{{ client_backup_ssh_key }}"
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: "0600"
|
||||||
|
no_log: true
|
||||||
|
|
||||||
|
- name: Déposer le mot de passe restic
|
||||||
|
ansible.builtin.copy:
|
||||||
|
content: "{{ client_backup_password }}\n"
|
||||||
|
dest: /etc/setops/restic.pass
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: "0600"
|
||||||
|
no_log: true
|
||||||
|
|
||||||
|
- name: Assurer /root/.ssh
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: /root/.ssh
|
||||||
|
state: directory
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: "0700"
|
||||||
|
|
||||||
|
- name: Configurer l'accès SSH à la cible de sauvegarde
|
||||||
|
ansible.builtin.blockinfile:
|
||||||
|
path: /root/.ssh/config
|
||||||
|
create: true
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: "0600"
|
||||||
|
marker: "# {mark} SETOPS BACKUP {{ client_backup_cible }}"
|
||||||
|
block: |
|
||||||
|
Host {{ client_backup_cible }}
|
||||||
|
User {{ client_backup_utilisateur_distant }}
|
||||||
|
IdentityFile {{ client_backup_ssh_key }}
|
||||||
|
IdentitiesOnly yes
|
||||||
|
StrictHostKeyChecking accept-new
|
||||||
|
|
||||||
- name: Assurer le répertoire de préparation des dumps
|
- name: Assurer le répertoire de préparation des dumps
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
|
|
@ -56,17 +106,6 @@
|
||||||
group: root
|
group: root
|
||||||
mode: "0700"
|
mode: "0700"
|
||||||
|
|
||||||
# LA COPIE A FROID (2026-10-07) : les jeux qui s'ecrivent en continu (Prometheus, Loki)
|
|
||||||
# arretent leur service le temps de copier. L'outil est deploye avec le script qui l'appelle.
|
|
||||||
- name: Déployer l'outil de copie à froid
|
|
||||||
when: client_backup_jobs | length > 0
|
|
||||||
ansible.builtin.copy:
|
|
||||||
src: setops-copie-a-froid.sh
|
|
||||||
dest: /usr/local/sbin/setops-copie-a-froid
|
|
||||||
owner: root
|
|
||||||
group: root
|
|
||||||
mode: "0700"
|
|
||||||
|
|
||||||
- name: Déployer l'unité et le timer systemd
|
- name: Déployer l'unité et le timer systemd
|
||||||
when: client_backup_jobs | length > 0
|
when: client_backup_jobs | length > 0
|
||||||
ansible.builtin.template:
|
ansible.builtin.template:
|
||||||
|
|
@ -78,7 +117,6 @@
|
||||||
loop:
|
loop:
|
||||||
- { s: setops-sauvegarde.service.j2, d: setops-sauvegarde.service }
|
- { s: setops-sauvegarde.service.j2, d: setops-sauvegarde.service }
|
||||||
- { s: setops-sauvegarde.timer.j2, d: setops-sauvegarde.timer }
|
- { s: setops-sauvegarde.timer.j2, d: setops-sauvegarde.timer }
|
||||||
- { s: setops-sauvegarde-avant-raser.service.j2, d: setops-sauvegarde-avant-raser.service }
|
|
||||||
|
|
||||||
- name: Activer le timer de sauvegarde
|
- name: Activer le timer de sauvegarde
|
||||||
when:
|
when:
|
||||||
|
|
@ -102,24 +140,6 @@
|
||||||
state: stopped
|
state: stopped
|
||||||
failed_when: false
|
failed_when: false
|
||||||
|
|
||||||
# LES VERIFICATIONS PARTENT AVEC LA SAUVEGARDE (2026-09-30). Retirees seules, les unites de
|
|
||||||
# sauvegarde laissaient derriere elles celles qui VERIFIENT le depot et la restauration :
|
|
||||||
# elles continuaient de rapporter a des services qu'Icinga ne declare plus (le noeud n'est
|
|
||||||
# plus detenteur d'etat) — un 404 toutes les quatre heures, et une unite rouge. Vu en
|
|
||||||
# retirant le web frontal du catalogue.
|
|
||||||
- name: Arreter les verifications d'une sauvegarde devenue vide
|
|
||||||
when:
|
|
||||||
- client_backup_jobs | length == 0
|
|
||||||
- not ansible_check_mode
|
|
||||||
ansible.builtin.systemd:
|
|
||||||
name: "{{ item }}"
|
|
||||||
enabled: false
|
|
||||||
state: stopped
|
|
||||||
loop:
|
|
||||||
- setops-verification-depot.timer
|
|
||||||
- setops-verification-restauration.timer
|
|
||||||
failed_when: false
|
|
||||||
|
|
||||||
- name: Retirer le script et les unites d'une sauvegarde vide
|
- name: Retirer le script et les unites d'une sauvegarde vide
|
||||||
when: client_backup_jobs | length == 0
|
when: client_backup_jobs | length == 0
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
|
|
@ -127,16 +147,8 @@
|
||||||
state: absent
|
state: absent
|
||||||
loop:
|
loop:
|
||||||
- /usr/local/sbin/setops-sauvegarder.sh
|
- /usr/local/sbin/setops-sauvegarder.sh
|
||||||
- /usr/local/sbin/setops-copie-a-froid
|
|
||||||
- /etc/systemd/system/setops-sauvegarde.service
|
- /etc/systemd/system/setops-sauvegarde.service
|
||||||
- /etc/systemd/system/setops-sauvegarde.timer
|
- /etc/systemd/system/setops-sauvegarde.timer
|
||||||
- /etc/systemd/system/setops-sauvegarde-avant-raser.service
|
|
||||||
- /usr/local/sbin/setops-verifier-mon-depot.sh
|
|
||||||
- /usr/local/sbin/setops-verifier-restauration.sh
|
|
||||||
- /etc/systemd/system/setops-verification-depot.service
|
|
||||||
- /etc/systemd/system/setops-verification-depot.timer
|
|
||||||
- /etc/systemd/system/setops-verification-restauration.service
|
|
||||||
- /etc/systemd/system/setops-verification-restauration.timer
|
|
||||||
notify: Recharger systemd
|
notify: Recharger systemd
|
||||||
|
|
||||||
# --- LE NOEUD VERIFIE SON PROPRE DEPOT ----------------------------------------
|
# --- LE NOEUD VERIFIE SON PROPRE DEPOT ----------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -1,98 +0,0 @@
|
||||||
---
|
|
||||||
# L'ETAT D'UNE INCARNATION PRECEDENTE ATTEND-IL CE JEU ?
|
|
||||||
#
|
|
||||||
# Inclus par le role PROPRIETAIRE de l'etat, au moment precis ou il le creerait neuf :
|
|
||||||
# l'AC avant `step ca init`, les bases juste creees, l'annuaire une fois son schema pose...
|
|
||||||
# C'est lui qui sait quand son etat est « vierge » ; ce fichier sait ou est le depot.
|
|
||||||
#
|
|
||||||
# entree : client_backup_restaurer_jeu — groupe du catalogue (ex. serveur_step_ca)
|
|
||||||
# client_backup_restaurer_vierge — le role n'a encore RIEN de cet etat
|
|
||||||
# sortie : client_backup_restauration — {jeu, etat, instantane, chemins}
|
|
||||||
#
|
|
||||||
# etat : `a_restaurer` (au role de le remettre, puis d'inclure `acter.yml`), `acte`
|
|
||||||
# (deja tranche a une passe precedente), `en_place` (un etat existe : on ne l'ecrase
|
|
||||||
# jamais d'office), `neuf` (rien d'anterieur), `sans_objet`, `simulation`.
|
|
||||||
- name: Restauration — le jeu et ce qu'il emporte
|
|
||||||
ansible.builtin.set_fact:
|
|
||||||
client_backup_restauration:
|
|
||||||
jeu: "{{ client_backup_catalogue[client_backup_restaurer_jeu].nom }}"
|
|
||||||
chemins: "{{ client_backup_catalogue[client_backup_restaurer_jeu].chemins }}"
|
|
||||||
etat: >-
|
|
||||||
{{ 'simulation' if ansible_check_mode
|
|
||||||
else ('sans_objet' if ('client_backup' not in group_names)
|
|
||||||
else ('desactive' if not (client_backup_restauration_active | bool)
|
|
||||||
else 'a_decider')) }}
|
|
||||||
instantane: ""
|
|
||||||
|
|
||||||
- name: Restauration — trancher pour ce jeu
|
|
||||||
when: client_backup_restauration.etat != 'sans_objet' and client_backup_restauration.etat != 'simulation'
|
|
||||||
block:
|
|
||||||
- name: Restauration — ce jeu a-t-il deja ete tranche ?
|
|
||||||
ansible.builtin.slurp:
|
|
||||||
src: "{{ client_backup_restauration_marques }}/{{ client_backup_restauration.jeu }}"
|
|
||||||
register: client_backup_marque_lue
|
|
||||||
failed_when: false
|
|
||||||
|
|
||||||
- name: Restauration — deja tranche a une passe precedente
|
|
||||||
ansible.builtin.set_fact:
|
|
||||||
client_backup_restauration: >-
|
|
||||||
{{ client_backup_restauration | combine({'etat': 'acte',
|
|
||||||
'instantane': ((client_backup_marque_lue.content | b64decode).splitlines()
|
|
||||||
| select('match', '^instantane=') | first
|
|
||||||
| default('instantane=')).split('=', 1)[1]}) }}
|
|
||||||
when: client_backup_marque_lue.content is defined
|
|
||||||
|
|
||||||
- name: "Restauration — desactivee, ou un etat existe deja, l'acter tel quel"
|
|
||||||
ansible.builtin.include_tasks: acter.yml
|
|
||||||
vars:
|
|
||||||
client_backup_acter_etat: "{{ 'desactive' if client_backup_restauration.etat == 'desactive' else 'en_place' }}"
|
|
||||||
when:
|
|
||||||
- client_backup_marque_lue.content is not defined
|
|
||||||
- client_backup_restauration.etat == 'desactive' or not (client_backup_restaurer_vierge | bool)
|
|
||||||
|
|
||||||
# LA CONDITION D'UN BLOC SE REEVALUE A CHAQUE TACHE. Ecrite sur `etat`, elle se
|
|
||||||
# fermerait des que le choix le change — et « rien d'anterieur » ne serait jamais acte.
|
|
||||||
# On la fige donc avant d'entrer.
|
|
||||||
- name: Restauration — faut-il interroger le depot ?
|
|
||||||
ansible.builtin.set_fact:
|
|
||||||
client_backup_interroger: >-
|
|
||||||
{{ client_backup_marque_lue.content is not defined
|
|
||||||
and client_backup_restauration.etat == 'a_decider'
|
|
||||||
and (client_backup_restaurer_vierge | bool) }}
|
|
||||||
|
|
||||||
- name: Restauration — interroger le depot
|
|
||||||
when: client_backup_interroger | bool
|
|
||||||
block:
|
|
||||||
- name: Restauration — acces au depot (le role client_backup n'a peut-etre pas encore tourne)
|
|
||||||
ansible.builtin.include_tasks: acces.yml
|
|
||||||
|
|
||||||
# Code 2 = depot ILLISIBLE : on echoue. Conclure « rien a restaurer » faute de
|
|
||||||
# joindre le depot ferait naitre neuf ce qui attend chez l'hebergeur.
|
|
||||||
- name: Restauration — l'instantane de l'incarnation precedente
|
|
||||||
ansible.builtin.command:
|
|
||||||
argv: >-
|
|
||||||
{{ ['/usr/local/sbin/setops-restaurer', 'choisir']
|
|
||||||
+ (['--instantane', client_backup_restauration_instantane]
|
|
||||||
if client_backup_restauration_instantane | length > 0 else []) }}
|
|
||||||
register: client_backup_choix
|
|
||||||
changed_when: false
|
|
||||||
|
|
||||||
- name: Restauration — retenir le choix
|
|
||||||
ansible.builtin.set_fact:
|
|
||||||
client_backup_restauration: >-
|
|
||||||
{{ client_backup_restauration | combine({
|
|
||||||
'etat': ('a_restaurer' if (client_backup_choix.stdout | from_json).instantane else 'neuf'),
|
|
||||||
'instantane': (client_backup_choix.stdout | from_json).instantane}) }}
|
|
||||||
|
|
||||||
- name: "Restauration — rien d'anterieur, naissance"
|
|
||||||
ansible.builtin.include_tasks: acter.yml
|
|
||||||
vars:
|
|
||||||
client_backup_acter_etat: neuf
|
|
||||||
when: client_backup_restauration.etat == 'neuf'
|
|
||||||
|
|
||||||
- name: Restauration — verdict
|
|
||||||
ansible.builtin.debug:
|
|
||||||
msg: >-
|
|
||||||
{{ client_backup_restauration.jeu }} : {{ client_backup_restauration.etat }}{{
|
|
||||||
(' — instantane ' + client_backup_restauration.instantane)
|
|
||||||
if client_backup_restauration.instantane | length > 0 else '' }}
|
|
||||||
|
|
@ -46,45 +46,6 @@
|
||||||
group: root
|
group: root
|
||||||
mode: "0644"
|
mode: "0644"
|
||||||
|
|
||||||
# UN ICINGA QUI NE NOUS A JAMAIS ENTENDUS (2026-09-30). Ses services `sauvegarde` et
|
|
||||||
# `restauration` sont CRITIQUES tant qu'aucun rapport n'est arrive — c'est voulu, le silence
|
|
||||||
# doit alerter. Mais les minuteurs ne rapportent que toutes les 4 h (depot) et le DIMANCHE
|
|
||||||
# (restauration) : apres chaque reconstruction, Icinga restait rouge jusqu'a une semaine.
|
|
||||||
#
|
|
||||||
# Le noeud ne peut pas demander a Icinga s'il l'a deja entendu : son compte ne sait que
|
|
||||||
# DEPOSER. Il retient donc LUI-MEME a quel Icinga il a rapporte — l'empreinte de son AC. Un
|
|
||||||
# Icinga refait a une AC neuve ; un noeud refait n'a pas de marqueur.
|
|
||||||
#
|
|
||||||
# UN MARQUEUR A LUI, PAS LA COPIE DE L'AC. La premiere version se fiait au changement de
|
|
||||||
# `icinga-ca.crt` — or `client_sante` depose le MEME fichier, plus tot dans le deploiement :
|
|
||||||
# sur une flotte neuve il etait deja la, « ok », et rien n'etait notifie. Vu a la
|
|
||||||
# reconstruction de Chezlepro du 2026-09-30 : `restauration` rouge sur sept noeuds, et le
|
|
||||||
# pare-feu arrete. L'epreuve precedente avait joue `client_backup` SEUL, et ne pouvait pas
|
|
||||||
# le voir.
|
|
||||||
#
|
|
||||||
# ECRIT APRES LE RAPPORT (dernier gestionnaire), jamais avant : un rapport rate laisserait
|
|
||||||
# sinon le noeud convaincu d'avoir ete entendu, et la passe suivante ne retenterait rien.
|
|
||||||
- name: Retenir l'empreinte de l'Icinga destinataire
|
|
||||||
when: client_backup_ca_presente.stat.exists
|
|
||||||
ansible.builtin.set_fact:
|
|
||||||
client_backup_icinga_empreinte: "{{ client_backup_ca_icinga.content | b64decode | hash('sha256') }}"
|
|
||||||
|
|
||||||
- name: A quel Icinga ce noeud a-t-il deja rapporte ?
|
|
||||||
when: client_backup_ca_presente.stat.exists
|
|
||||||
ansible.builtin.slurp:
|
|
||||||
src: "{{ client_backup_marque_icinga }}"
|
|
||||||
register: client_backup_icinga_entendu
|
|
||||||
failed_when: false
|
|
||||||
|
|
||||||
- name: Cet Icinga ne nous a jamais entendus — premier rapport
|
|
||||||
when:
|
|
||||||
- client_backup_ca_presente.stat.exists
|
|
||||||
- (client_backup_icinga_entendu.content | default('') | b64decode | trim) != client_backup_icinga_empreinte
|
|
||||||
ansible.builtin.debug:
|
|
||||||
msg: "Icinga {{ client_backup_icinga_empreinte[:12] }} n'a encore rien recu de {{ inventory_hostname }} : rapport immediat."
|
|
||||||
changed_when: true
|
|
||||||
notify: Premier rapport a Icinga
|
|
||||||
|
|
||||||
- name: Installer curl pour le rapport passif
|
- name: Installer curl pour le rapport passif
|
||||||
ansible.builtin.apt:
|
ansible.builtin.apt:
|
||||||
name: curl
|
name: curl
|
||||||
|
|
|
||||||
|
|
@ -1,417 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
# GENERE par Set-OPS (role client_backup). Ne pas editer a la main.
|
|
||||||
#
|
|
||||||
# setops-restaurer — REMETTRE EN SERVICE l'etat d'une incarnation precedente de CE noeud.
|
|
||||||
#
|
|
||||||
# POURQUOI IL EXISTE (2026-09-30). Technolibre a ete rasee et reconstruite par ses
|
|
||||||
# runners : 0 echec, `make valider` vert, « restauration verifiee ». Et le mot de passe de
|
|
||||||
# `sysadmin` etait revenu a celui de l'amorcage. RIEN n'etait revenu : ni l'annuaire, ni
|
|
||||||
# la racine de l'AC, ni les bases, ni les fichiers de Nextcloud. « Restauration verifiee »
|
|
||||||
# voulait dire : restauree dans un repertoire temporaire, lue, jetee. Chaque
|
|
||||||
# reconstruction de Chezlepro etait repartie a vide de la meme facon.
|
|
||||||
#
|
|
||||||
# QUELLE INCARNATION. La cle d'hote SSH nait au clonage (`machine-id`, lui, vient du
|
|
||||||
# gabarit : tous les noeuds portent la meme date). L'instantane a remettre est donc le
|
|
||||||
# DERNIER PRIS AVANT LA NAISSANCE de cette cle — celui de la machine qu'on a rasee.
|
|
||||||
# `--instantane ID` impose un autre choix.
|
|
||||||
#
|
|
||||||
# LE MARQUEUR. Un jeu restaure, abandonne, ou trouve deja en place laisse une trace dans
|
|
||||||
# {{ client_backup_restauration_marques }}/<jeu>. La sauvegarde s'y fie (`garde`) : tant
|
|
||||||
# qu'un etat anterieur n'a ete ni remis ni ecarte, ce noeud REFUSE de deposer — sans quoi
|
|
||||||
# `restic forget --keep-daily` chasserait l'instantane d'avant au profit de l'etat neuf du
|
|
||||||
# meme jour.
|
|
||||||
#
|
|
||||||
# RIEN N'EST ECRASE SANS ETRE MIS DE COTE, dans {{ client_backup_restauration_mise_de_cote }}.
|
|
||||||
#
|
|
||||||
# Sous-commandes :
|
|
||||||
# etat ce qui serait restaure, et les marqueurs
|
|
||||||
# choisir [--instantane ID] (pour Ansible) JSON de l'instantane candidat
|
|
||||||
# fichiers [opts] CHEMIN... remettre des repertoires (rsync --delete)
|
|
||||||
# --proprietaire U:G --remplacer --vers DOSSIER (repetition, rien n'est touche)
|
|
||||||
# --depuis CHEMIN_DANS_L_INSTANTANE (un seul CHEMIN) : ce qui a ete sauvegarde
|
|
||||||
# ailleurs que la ou ca vit (copie a froid)
|
|
||||||
# annuaire [opts] BASE_DN remplacer la base LDAP (slapadd)
|
|
||||||
# --essai (slapadd -u a blanc, rien n'est touche)
|
|
||||||
# base [opts] NOM rejouer une base depuis le pg_dumpall
|
|
||||||
# --remplacer --vers AUTRE_NOM (repetition dans une base jetable)
|
|
||||||
# acter JEU ETAT [INSTANTANE] poser le marqueur (restaure, neuf, en_place, abandonne)
|
|
||||||
# garde (pour la sauvegarde) refuser si un etat attend
|
|
||||||
# temoins [--candidat | --instantane ID] l'etat vivant contre l'instantane d'avant rasage
|
|
||||||
# (par defaut le dernier etiquete « avant-raser ») ; lecture seule ; code 1 = ecart,
|
|
||||||
# 4 = rien a comparer
|
|
||||||
# Options communes : --instantane ID.
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
export RESTIC_REPOSITORY="{{ client_backup_repo }}"
|
|
||||||
export RESTIC_PASSWORD_FILE="/etc/setops/restic.pass"
|
|
||||||
# UN VERROU N'EST PAS UNE PANNE (2026-10-04). La sauvegarde, la verification du depot, le
|
|
||||||
# controle de restauration et `setops-restaurer` partagent ce depot ; leurs minuteurs peuvent
|
|
||||||
# partir ensemble (`Persistent=true` rattrape au demarrage tout ce qui a ete manque). Sans
|
|
||||||
# attente, restic refuse aussitot un depot verrouille, et une sonde concluait « aucun
|
|
||||||
# instantane » ou « depot corrompu ». On attend le verrou au lieu de conclure.
|
|
||||||
restic() { command restic --retry-lock {{ client_backup_attente_verrou }} "$@"; }
|
|
||||||
MOI="{{ inventory_hostname }}"
|
|
||||||
MARQUES="{{ client_backup_restauration_marques }}"
|
|
||||||
MISE_DE_COTE="{{ client_backup_restauration_mise_de_cote }}"
|
|
||||||
CLE_HOTE="/etc/ssh/ssh_host_ed25519_key.pub"
|
|
||||||
LDIF="{{ client_backup_staging }}/openldap/annuaire.ldif"
|
|
||||||
DUMP_PG="{{ client_backup_staging }}/postgresql/toutes-bases.sql"
|
|
||||||
JEUX=({% for j in client_backup_jobs %} "{{ j.nom }}"{% endfor %} )
|
|
||||||
# Les chemins de chaque jeu, separes par « : » — ce que les temoins extraient et comparent.
|
|
||||||
declare -A CHEMINS_JEU=({% for j in client_backup_jobs %} ["{{ j.nom }}"]="{{ j.chemins | join(':') }}"{% endfor %} )
|
|
||||||
ETIQUETTE_AVANT_RASER="{{ client_backup_etiquette_avant_raser }}"
|
|
||||||
# Ou vit la base des jeux copies a froid (Prometheus, Loki) : leur instantane porte la
|
|
||||||
# copie, pas la base. Les temoins comparent l'une a l'autre.
|
|
||||||
declare -A VIVANT_JEU=({% for j in client_backup_jobs if j.vivant is defined %} ["{{ j.nom }}"]="{{ j.vivant }}"{% endfor %} )
|
|
||||||
|
|
||||||
dire() { printf '%s\n' "$*" >&2; }
|
|
||||||
mourir() { dire "setops-restaurer: $*"; exit 1; }
|
|
||||||
|
|
||||||
naissance() {
|
|
||||||
local b
|
|
||||||
b=$(stat -c %W "${CLE_HOTE}")
|
|
||||||
if [[ "${b}" -le 0 ]]; then b=$(stat -c %Y "${CLE_HOTE}"); fi
|
|
||||||
printf '%s\n' "${b}"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Imprime « ID HEURE » du candidat, ou rien. Code 2 : depot illisible — et c'est une
|
|
||||||
# ERREUR, pas une absence : conclure « rien a restaurer » parce que le dépôt ne repond pas
|
|
||||||
# ferait marquer « neuf » une machine dont l'etat attend chez l'hebergeur.
|
|
||||||
candidat() {
|
|
||||||
local impose="${1:-}" json rc
|
|
||||||
set +e
|
|
||||||
json=$(restic snapshots --host "${MOI}" --json 2>/dev/null)
|
|
||||||
rc=$?
|
|
||||||
set -e
|
|
||||||
# restic 0.17+ : 10 = le depot n'existe pas. Premiere vie de l'ecosysteme.
|
|
||||||
if (( rc == 10 )); then return 0; fi
|
|
||||||
if (( rc != 0 )); then dire "depot illisible (restic rc=${rc}) : ${RESTIC_REPOSITORY}"; return 2; fi
|
|
||||||
printf '%s' "${json}" | python3 -c '
|
|
||||||
import datetime, json, re, sys
|
|
||||||
naissance, impose = int(sys.argv[1]), sys.argv[2]
|
|
||||||
def t(s):
|
|
||||||
s = re.sub(r"(\.\d{6})\d+", r"\1", s)
|
|
||||||
return datetime.datetime.fromisoformat(re.sub(r"Z$", "+00:00", s)).timestamp()
|
|
||||||
snaps = json.load(sys.stdin) or []
|
|
||||||
if impose:
|
|
||||||
c = [x for x in snaps if impose in (x["short_id"], x["id"])]
|
|
||||||
if not c:
|
|
||||||
sys.exit("instantane impose introuvable pour cet hote : " + impose)
|
|
||||||
else:
|
|
||||||
c = [x for x in snaps if t(x["time"]) < naissance]
|
|
||||||
c.sort(key=lambda x: t(x["time"]))
|
|
||||||
if c:
|
|
||||||
print(c[-1]["short_id"], c[-1]["time"][:19])
|
|
||||||
' "$(naissance)" "${impose}"
|
|
||||||
}
|
|
||||||
|
|
||||||
# « ID HEURE » du dernier instantane etiquete avant rasage, ou rien ; code 2 = depot illisible.
|
|
||||||
avant_raser() {
|
|
||||||
local json rc
|
|
||||||
set +e
|
|
||||||
json=$(restic snapshots --host "${MOI}" --tag "${ETIQUETTE_AVANT_RASER}" --json 2>/dev/null)
|
|
||||||
rc=$?
|
|
||||||
set -e
|
|
||||||
if (( rc == 10 )); then return 0; fi
|
|
||||||
if (( rc != 0 )); then dire "depot illisible (restic rc=${rc}) : ${RESTIC_REPOSITORY}"; return 2; fi
|
|
||||||
printf '%s' "${json}" | python3 -c '
|
|
||||||
import json, sys
|
|
||||||
import datetime, re
|
|
||||||
def t(x):
|
|
||||||
s = re.sub(r"(\.\d{6})\d+", r"\1", x["time"])
|
|
||||||
return datetime.datetime.fromisoformat(re.sub(r"Z$", "+00:00", s)).timestamp()
|
|
||||||
s = sorted(json.load(sys.stdin) or [], key=t)
|
|
||||||
if s:
|
|
||||||
print(s[-1]["short_id"], s[-1]["time"][:19])
|
|
||||||
'
|
|
||||||
}
|
|
||||||
|
|
||||||
# Instantane a utiliser : impose, sinon le candidat. Vide = rien a restaurer.
|
|
||||||
INSTANTANE=""
|
|
||||||
resoudre_instantane() {
|
|
||||||
local c
|
|
||||||
c=$(candidat "${INSTANTANE}") || exit 2
|
|
||||||
INSTANTANE="${c%% *}"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Le repertoire jetable ou l'instantane est extrait. Detruit a la sortie, quoi qu'il arrive.
|
|
||||||
TMP=""
|
|
||||||
trap 'if [[ -n "${TMP}" ]]; then rm -rf "${TMP}"; fi' EXIT
|
|
||||||
|
|
||||||
extraire() { # remplit TMP avec les chemins demandes de l'instantane
|
|
||||||
local inc=()
|
|
||||||
TMP=$(mktemp -d /var/tmp/setops-restauration.XXXXXX)
|
|
||||||
for c in "$@"; do inc+=(--include "${c}"); done
|
|
||||||
restic restore "${INSTANTANE}" --target "${TMP}" "${inc[@]}" >/dev/null \
|
|
||||||
|| mourir "restic restore ${INSTANTANE} a echoue"
|
|
||||||
}
|
|
||||||
|
|
||||||
MIS_DE_COTE=""
|
|
||||||
mettre_de_cote() { # $1 = chemin vivant ; MIS_DE_COTE = ou il est parti
|
|
||||||
MIS_DE_COTE="${MISE_DE_COTE}/$(date +%Y%m%d-%H%M%S)$1"
|
|
||||||
mkdir -p "$(dirname "${MIS_DE_COTE}")"
|
|
||||||
cp -a "$1" "${MIS_DE_COTE}"
|
|
||||||
dire "etat en place mis de cote : ${MIS_DE_COTE}"
|
|
||||||
}
|
|
||||||
|
|
||||||
vide() { # vrai si le chemin est absent, ou un repertoire sans contenu
|
|
||||||
[[ ! -e "$1" ]] || { [[ -d "$1" ]] && [[ -z "$(find "$1" -mindepth 1 -print -quit)" ]]; }
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd_fichiers() {
|
|
||||||
local proprio="" remplacer=0 vers="" depuis="" chemins=() src dest n
|
|
||||||
while (( $# )); do
|
|
||||||
case "$1" in
|
|
||||||
--proprietaire) proprio="$2"; shift 2 ;;
|
|
||||||
--remplacer) remplacer=1; shift ;;
|
|
||||||
--vers) vers="$2"; shift 2 ;;
|
|
||||||
--depuis) depuis="$2"; shift 2 ;;
|
|
||||||
--instantane) INSTANTANE="$2"; shift 2 ;;
|
|
||||||
*) chemins+=("$1"); shift ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
[[ -n "${chemins[*]:-}" ]] || mourir "fichiers : aucun chemin"
|
|
||||||
# LA COPIE A FROID (2026-10-07) : l'instantane porte la copie posee dans le repertoire de
|
|
||||||
# preparation, pas la base. `--depuis` dit ou la prendre ; le CHEMIN, ou la remettre.
|
|
||||||
# (Un second element, plutot qu'une longueur : la sequence accolade-diese ouvre un
|
|
||||||
# commentaire Jinja dans ce gabarit.)
|
|
||||||
if [[ -n "${depuis}" && -n "${chemins[1]:-}" ]]; then mourir "fichiers --depuis : un seul chemin"; fi
|
|
||||||
resoudre_instantane
|
|
||||||
if [[ -z "${INSTANTANE}" ]]; then echo "RIEN : aucune incarnation precedente."; return 0; fi
|
|
||||||
if [[ -n "${depuis}" ]]; then extraire "${depuis}"; else extraire "${chemins[@]}"; fi
|
|
||||||
for c in "${chemins[@]}"; do
|
|
||||||
src="${TMP}${depuis:-${c}}"
|
|
||||||
dest="${vers}${c}"
|
|
||||||
if [[ ! -e "${src}" ]]; then echo "ABSENT de ${INSTANTANE} : ${c}"; continue; fi
|
|
||||||
if ! vide "${dest}"; then
|
|
||||||
if [[ -z "${vers}" ]] && (( ! remplacer )); then
|
|
||||||
mourir "${dest} n'est pas vide : --remplacer pour l'ecraser (il sera mis de cote)"
|
|
||||||
fi
|
|
||||||
mettre_de_cote "${dest}"
|
|
||||||
fi
|
|
||||||
mkdir -p "$(dirname "${dest}")"
|
|
||||||
if [[ -d "${src}" ]]; then
|
|
||||||
mkdir -p "${dest}"
|
|
||||||
rsync -a --delete "${src}/" "${dest}/"
|
|
||||||
else
|
|
||||||
cp -a "${src}" "${dest}"
|
|
||||||
fi
|
|
||||||
if [[ -n "${proprio}" ]]; then chown -R "${proprio}" "${dest}"; fi
|
|
||||||
n=$(find "${dest}" -type f | wc -l)
|
|
||||||
echo "RESTAURE ${INSTANTANE} : ${c} -> ${dest} (${n} fichier(s))"
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd_annuaire() {
|
|
||||||
local essai=0 base="" ldif dir n
|
|
||||||
while (( $# )); do
|
|
||||||
case "$1" in
|
|
||||||
--essai) essai=1; shift ;;
|
|
||||||
--instantane) INSTANTANE="$2"; shift 2 ;;
|
|
||||||
*) base="$1"; shift ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
[[ -n "${base}" ]] || mourir "annuaire : BASE_DN requis"
|
|
||||||
resoudre_instantane
|
|
||||||
if [[ -z "${INSTANTANE}" ]]; then echo "RIEN : aucune incarnation precedente."; return 0; fi
|
|
||||||
extraire "$(dirname "${LDIF}")"
|
|
||||||
ldif="${TMP}${LDIF}"
|
|
||||||
[[ -s "${ldif}" ]] || mourir "${INSTANTANE} n'emporte pas d'annuaire (${LDIF})"
|
|
||||||
n=$(grep -c '^dn:' "${ldif}")
|
|
||||||
# A BLANC D'ABORD : un LDIF que le schema EN PLACE refuse (overlay pas encore charge,
|
|
||||||
# attribut inconnu) doit echouer ici, pas apres qu'on a deplace la base vivante.
|
|
||||||
slapadd -u -q -b "${base}" -l "${ldif}" || mourir "slapadd a blanc refuse ${INSTANTANE}"
|
|
||||||
if (( essai )); then echo "ESSAI OK : ${n} entree(s) rejouables depuis ${INSTANTANE}."; return 0; fi
|
|
||||||
dir=$(slapcat -n 0 -a "(olcSuffix=${base})" | awk '/^olcDbDirectory:/{print $2}')
|
|
||||||
[[ -d "${dir}" ]] || mourir "repertoire de la base introuvable pour ${base}"
|
|
||||||
systemctl stop slapd
|
|
||||||
mettre_de_cote "${dir}"
|
|
||||||
find "${dir}" -mindepth 1 -delete
|
|
||||||
if ! slapadd -q -b "${base}" -l "${ldif}"; then
|
|
||||||
dire "slapadd a echoue : remise de la base d'avant"
|
|
||||||
find "${dir}" -mindepth 1 -delete
|
|
||||||
cp -a "${MIS_DE_COTE}/." "${dir}/"
|
|
||||||
chown -R openldap:openldap "${dir}"
|
|
||||||
systemctl start slapd
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
chown -R openldap:openldap "${dir}"
|
|
||||||
systemctl start slapd
|
|
||||||
echo "RESTAURE ${INSTANTANE} : annuaire ${base}, ${n} entree(s)."
|
|
||||||
}
|
|
||||||
|
|
||||||
tables() { # nombre de tables hors schemas systeme ; -1 si la base n'existe pas
|
|
||||||
runuser -u postgres -- psql -tAX -d "$1" -c \
|
|
||||||
"select count(*) from information_schema.tables where table_schema not in ('pg_catalog','information_schema')" \
|
|
||||||
2>/dev/null || echo -1
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd_base() {
|
|
||||||
local remplacer=0 vers="" nom="" cible dump section creation proprio n
|
|
||||||
while (( $# )); do
|
|
||||||
case "$1" in
|
|
||||||
--remplacer) remplacer=1; shift ;;
|
|
||||||
--vers) vers="$2"; shift 2 ;;
|
|
||||||
--instantane) INSTANTANE="$2"; shift 2 ;;
|
|
||||||
*) nom="$1"; shift ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
[[ -n "${nom}" ]] || mourir "base : NOM requis"
|
|
||||||
cible="${vers:-${nom}}"
|
|
||||||
resoudre_instantane
|
|
||||||
if [[ -z "${INSTANTANE}" ]]; then echo "RIEN : aucune incarnation precedente."; return 0; fi
|
|
||||||
extraire "$(dirname "${DUMP_PG}")"
|
|
||||||
dump="${TMP}${DUMP_PG}"
|
|
||||||
[[ -s "${dump}" ]] || mourir "${INSTANTANE} n'emporte pas de dump PostgreSQL"
|
|
||||||
creation=$(grep -m1 -E "^CREATE DATABASE ${nom} " "${dump}" || true)
|
|
||||||
if [[ -z "${creation}" ]]; then echo "ABSENTE de ${INSTANTANE} : base ${nom}"; return 0; fi
|
|
||||||
proprio=$(grep -m1 -oP "^ALTER DATABASE ${nom} OWNER TO \K[^;]+" "${dump}" || echo postgres)
|
|
||||||
# LE PIEGE DE pg_dumpall (runbook §5) : un `CREATE DATABASE <suivante>` et, avec
|
|
||||||
# `--clean`, un `DROP DATABASE postgres;` precedent le `\connect` suivant. On coupe sur
|
|
||||||
# les trois, puis on REFUSE toute section qui viserait encore une autre base.
|
|
||||||
section="${TMP}/section.sql"
|
|
||||||
awk -v db="${nom}" '$0 == "\\connect " db {f=1; next}
|
|
||||||
f && (/^\\connect / || /^CREATE DATABASE / || /^DROP DATABASE /) {exit} f' "${dump}" > "${section}"
|
|
||||||
if grep -qE '^(DROP|CREATE|ALTER) DATABASE|^\\connect' "${section}"; then
|
|
||||||
mourir "section ${nom} hors perimetre : refus"
|
|
||||||
fi
|
|
||||||
if [[ -n "${vers}" ]]; then sed -i -E "/ ON DATABASE ${nom} /d" "${section}"; fi
|
|
||||||
n=$(tables "${cible}")
|
|
||||||
if (( n > 0 )) && (( ! remplacer )); then
|
|
||||||
mourir "la base ${cible} contient ${n} table(s) : --remplacer pour l'ecraser (elle sera mise de cote)"
|
|
||||||
fi
|
|
||||||
if (( n >= 0 )); then
|
|
||||||
mkdir -p "${MISE_DE_COTE}"
|
|
||||||
runuser -u postgres -- pg_dump -Fc "${cible}" > "${MISE_DE_COTE}/$(date +%Y%m%d-%H%M%S)-${cible}.dump"
|
|
||||||
runuser -u postgres -- dropdb --force "${cible}"
|
|
||||||
fi
|
|
||||||
runuser -u postgres -- psql -X -q -v ON_ERROR_STOP=1 -d postgres \
|
|
||||||
-c "${creation/CREATE DATABASE ${nom} /CREATE DATABASE ${cible} }" \
|
|
||||||
-c "ALTER DATABASE ${cible} OWNER TO ${proprio};"
|
|
||||||
# UNE SEULE TRANSACTION : une erreur au milieu laisse une base VIDE, que la prochaine
|
|
||||||
# passe retentera — pas une base a moitie chargee qu'on prendrait pour la bonne.
|
|
||||||
# Par l'ENTREE STANDARD, pas `-f` : psql tourne en `postgres`, qui ne peut pas lire le
|
|
||||||
# repertoire jetable de root (0700). Trouve en repetition sur data-sql-01, 2026-09-30.
|
|
||||||
runuser -u postgres -- psql -X -q --single-transaction -v ON_ERROR_STOP=1 \
|
|
||||||
-d "${cible}" < "${section}" >/dev/null
|
|
||||||
echo "RESTAURE ${INSTANTANE} : base ${nom} -> ${cible}, $(tables "${cible}") table(s)."
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd_acter() {
|
|
||||||
local jeu="${1:?jeu}" etat="${2:?etat}" id="${3:-}"
|
|
||||||
install -d -m 0700 "${MARQUES}"
|
|
||||||
printf 'etat=%s\ninstantane=%s\ndate=%s\n' "${etat}" "${id}" "$(date -Is)" > "${MARQUES}/${jeu}"
|
|
||||||
echo "ACTE ${jeu} : ${etat}${id:+ (${id})}"
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd_choisir() {
|
|
||||||
[[ "${1:-}" == "--instantane" ]] && INSTANTANE="${2:-}"
|
|
||||||
local c
|
|
||||||
c=$(candidat "${INSTANTANE}") || exit 2
|
|
||||||
python3 -c 'import json,sys; print(json.dumps({"instantane": sys.argv[1], "heure": sys.argv[2], "naissance": int(sys.argv[3])}))' \
|
|
||||||
"${c%% *}" "$( [[ -n "${c}" ]] && printf '%s' "${c#* }" )" "$(naissance)"
|
|
||||||
}
|
|
||||||
|
|
||||||
# LE CANDIDAT EST CALCULE MAINTENANT (2026-10-07). Il dit ce qu'une restauration lancee a
|
|
||||||
# cette minute choisirait, pas ce qui a ete restaure : au bilan de M4, il nommait
|
|
||||||
# l'instantane de la veille, et l'on a cru un moment que la journee etait perdue. Ce qui a
|
|
||||||
# ete restaure est dans le marqueur de chaque jeu ; on dit desormais s'il est encore au depot.
|
|
||||||
cmd_etat() {
|
|
||||||
local c a ids id present
|
|
||||||
echo "hote : ${MOI}"
|
|
||||||
echo "naissance : $(date -d "@$(naissance)" -Is)"
|
|
||||||
c=$(candidat "") || exit 2
|
|
||||||
# (Pas d'apostrophe dans `${c:-...}` : entre guillemets doubles, bash l'y lit comme un
|
|
||||||
# guillemet ouvrant, et tout le script devient illisible.)
|
|
||||||
if [[ -z "${c}" ]]; then c="aucun (premiere vie, ou rien avant la naissance)"; fi
|
|
||||||
echo "candidat : ${c} (ce que choisirait une restauration lancee maintenant)"
|
|
||||||
a=$(avant_raser) || exit 2
|
|
||||||
echo "avant-raser: ${a:-aucun}"
|
|
||||||
ids=$(restic snapshots --host "${MOI}" --json 2>/dev/null \
|
|
||||||
| python3 -c 'import json, sys; print(" ".join(x["short_id"] for x in json.load(sys.stdin) or []))' \
|
|
||||||
2>/dev/null || true)
|
|
||||||
for j in "${JEUX[@]}"; do
|
|
||||||
if [[ -f "${MARQUES}/${j}" ]]; then
|
|
||||||
id=$(sed -n 's/^instantane=//p' "${MARQUES}/${j}")
|
|
||||||
present=""
|
|
||||||
if [[ -n "${id}" ]]; then
|
|
||||||
if [[ " ${ids} " == *" ${id} "* ]]; then present="(au depot)"; else present="(RETIRE du depot)"; fi
|
|
||||||
fi
|
|
||||||
echo "jeu ${j} : $(tr '\n' ' ' < "${MARQUES}/${j}")${present}"
|
|
||||||
else
|
|
||||||
echo "jeu ${j} : EN ATTENTE (aucun marqueur)"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
# LES TEMOINS (2026-10-07). L'etat vivant est-il celui d'avant le rasage ? On extrait
|
|
||||||
# l'instantane d'avant (etiquete, sinon le candidat ou celui qu'on impose), on lit
|
|
||||||
# l'annuaire vivant, et `setops-temoins` compare. Rien n'est ecrit hors du repertoire jetable.
|
|
||||||
cmd_temoins() {
|
|
||||||
local source="avant-raser" c heure base chemins=() args=()
|
|
||||||
while (( $# )); do
|
|
||||||
case "$1" in
|
|
||||||
--candidat) source="candidat"; shift ;;
|
|
||||||
--instantane) INSTANTANE="$2"; source="impose"; shift 2 ;;
|
|
||||||
*) mourir "temoins : option inconnue : $1" ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
if [[ -z "${JEUX[*]:-}" ]]; then echo "SANS OBJET : ${MOI} ne detient aucun etat."; exit 4; fi
|
|
||||||
case "${source}" in
|
|
||||||
avant-raser) c=$(avant_raser) || exit 2 ;;
|
|
||||||
candidat) c=$(candidat "") || exit 2 ;;
|
|
||||||
impose) c=$(candidat "${INSTANTANE}") || exit 2 ;;
|
|
||||||
esac
|
|
||||||
if [[ -z "${c}" ]]; then echo "SANS OBJET : aucun instantane ${source} pour ${MOI}."; exit 4; fi
|
|
||||||
INSTANTANE="${c%% *}"
|
|
||||||
heure="${c#* }"
|
|
||||||
for j in "${JEUX[@]}"; do
|
|
||||||
IFS=: read -r -a morceaux <<< "${CHEMINS_JEU[${j}]}"
|
|
||||||
chemins+=("${morceaux[@]}")
|
|
||||||
args+=(--jeu "${j}=${CHEMINS_JEU[${j}]}")
|
|
||||||
if [[ -n "${VIVANT_JEU[${j}]:-}" ]]; then args+=(--vivant "${j}=${VIVANT_JEU[${j}]}"); fi
|
|
||||||
done
|
|
||||||
extraire "${chemins[@]}"
|
|
||||||
if [[ -s "${TMP}${LDIF}" ]]; then
|
|
||||||
base=$(sed -n 's/^dn: //p' "${TMP}${LDIF}" | head -n 1)
|
|
||||||
slapcat -b "${base}" > "${TMP}/vivant-annuaire.ldif" || mourir "slapcat ${base} a echoue"
|
|
||||||
args+=(--ldif "${LDIF}" --ldif-vivant "${TMP}/vivant-annuaire.ldif")
|
|
||||||
fi
|
|
||||||
set +e
|
|
||||||
/usr/local/sbin/setops-temoins --avant "${TMP}" --instantane "${INSTANTANE}" \
|
|
||||||
--heure "${heure}" --hote "${MOI}" --dump "${DUMP_PG}" "${args[@]}"
|
|
||||||
local rc=$?
|
|
||||||
set -e
|
|
||||||
exit "${rc}"
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd_garde() {
|
|
||||||
local attente=() c
|
|
||||||
for j in "${JEUX[@]}"; do
|
|
||||||
[[ -f "${MARQUES}/${j}" ]] || attente+=("${j}")
|
|
||||||
done
|
|
||||||
if [[ -z "${attente[*]:-}" ]]; then return 0; fi
|
|
||||||
c=$(candidat "") || exit 2
|
|
||||||
if [[ -z "${c}" ]]; then
|
|
||||||
for j in "${attente[@]}"; do cmd_acter "${j}" neuf >/dev/null; done
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
dire "REFUS : l'etat d'une incarnation precedente (${c}) attend dans le depot, non remis"
|
|
||||||
dire "pour : ${attente[*]}. Deposer maintenant pousserait l'instantane d'avant hors de"
|
|
||||||
dire "la retention. Le remettre (redeployer le role proprietaire), ou l'ecarter :"
|
|
||||||
dire " make restauration-renoncer HOTE=${MOI} JEU=<jeu> CONFIRMER=true"
|
|
||||||
exit 3
|
|
||||||
}
|
|
||||||
|
|
||||||
sous="${1:-etat}"; shift || true
|
|
||||||
case "${sous}" in
|
|
||||||
etat) cmd_etat ;;
|
|
||||||
choisir) cmd_choisir "$@" ;;
|
|
||||||
fichiers) cmd_fichiers "$@" ;;
|
|
||||||
annuaire) cmd_annuaire "$@" ;;
|
|
||||||
base) cmd_base "$@" ;;
|
|
||||||
acter) cmd_acter "$@" ;;
|
|
||||||
garde) cmd_garde ;;
|
|
||||||
temoins) cmd_temoins "$@" ;;
|
|
||||||
*) mourir "sous-commande inconnue : ${sous}" ;;
|
|
||||||
esac
|
|
||||||
|
|
@ -4,31 +4,6 @@
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
export RESTIC_REPOSITORY="{{ client_backup_repo }}"
|
export RESTIC_REPOSITORY="{{ client_backup_repo }}"
|
||||||
export RESTIC_PASSWORD_FILE="/etc/setops/restic.pass"
|
export RESTIC_PASSWORD_FILE="/etc/setops/restic.pass"
|
||||||
# UN VERROU N'EST PAS UNE PANNE (2026-10-04). La sauvegarde, la verification du depot, le
|
|
||||||
# controle de restauration et `setops-restaurer` partagent ce depot ; leurs minuteurs peuvent
|
|
||||||
# partir ensemble (`Persistent=true` rattrape au demarrage tout ce qui a ete manque). Sans
|
|
||||||
# attente, restic refuse aussitot un depot verrouille, et une sonde concluait « aucun
|
|
||||||
# instantane » ou « depot corrompu ». On attend le verrou au lieu de conclure.
|
|
||||||
restic() { command restic --retry-lock {{ client_backup_attente_verrou }} "$@"; }
|
|
||||||
|
|
||||||
# L'INSTANTANE D'AVANT RASAGE (2026-10-07). `--avant-raser` (unite
|
|
||||||
# `setops-sauvegarde-avant-raser`, lancee par `make sauvegarder-maintenant`) etiquette le
|
|
||||||
# depot « {{ client_backup_etiquette_avant_raser }} », et la retention garde cette etiquette.
|
|
||||||
# Sans elle, le premier depot de la machine reconstruite, le meme jour, chassait l'instantane
|
|
||||||
# d'avant par `--keep-daily` : apres la reconstruction de Technolibre (M4), il ne restait
|
|
||||||
# rien a quoi comparer l'etat remis. Un seul porte l'etiquette, le dernier : il est garde
|
|
||||||
# jusqu'a la reconstruction suivante, qui la lui retire.
|
|
||||||
AVANT_RASER=0
|
|
||||||
if [[ "${1:-}" == "--avant-raser" ]]; then AVANT_RASER=1; fi
|
|
||||||
ETIQUETTES=(--tag setops)
|
|
||||||
if (( AVANT_RASER )); then ETIQUETTES+=(--tag "{{ client_backup_etiquette_avant_raser }}"); fi
|
|
||||||
|
|
||||||
# 0. UN ETAT D'AVANT ATTEND-IL ? (2026-09-30)
|
|
||||||
# Sur une machine reconstruite, tant que l'etat de l'incarnation precedente n'a ete ni
|
|
||||||
# remis ni ecarte, deposer serait NUISIBLE : `restic forget --keep-daily` garde un
|
|
||||||
# instantane par jour, et celui de l'etat neuf chasserait celui d'avant, s'ils tombent le
|
|
||||||
# meme jour. On refuse donc, bruyamment (code 3). Voir `setops-restaurer garde`.
|
|
||||||
/usr/local/sbin/setops-restaurer garde
|
|
||||||
|
|
||||||
# 1. Dumps applicatifs (pg_dump, slapcat, forgejo dump...) vers le staging.
|
# 1. Dumps applicatifs (pg_dump, slapcat, forgejo dump...) vers le staging.
|
||||||
{% for job in client_backup_jobs %}
|
{% for job in client_backup_jobs %}
|
||||||
|
|
@ -72,24 +47,7 @@ if (( N_PRESENTS == 0 )); then
|
||||||
echo "aucun chemin declare n'existe encore sur cet hote — rien a sauvegarder." >&2
|
echo "aucun chemin declare n'existe encore sur cet hote — rien a sauvegarder." >&2
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
restic backup --host "{{ inventory_hostname }}" "${ETIQUETTES[@]}" "${PRESENTS[@]}"
|
restic backup --host "{{ inventory_hostname }}" --tag setops "${PRESENTS[@]}"
|
||||||
|
|
||||||
# 3 bis. L'etiquette passe au dernier depot d'avant rasage : les precedents rentrent dans
|
|
||||||
# la retention ordinaire. APRES le depot, jamais avant : s'il echoue, l'ancien reste garde.
|
|
||||||
if (( AVANT_RASER )); then
|
|
||||||
ANCIENS=$(restic snapshots --host "{{ inventory_hostname }}" --tag "{{ client_backup_etiquette_avant_raser }}" --json \
|
|
||||||
| python3 -c 'import json, sys
|
|
||||||
import datetime, re
|
|
||||||
def t(x):
|
|
||||||
s = re.sub(r"(\.\d{6})\d+", r"\1", x["time"])
|
|
||||||
return datetime.datetime.fromisoformat(re.sub(r"Z$", "+00:00", s)).timestamp()
|
|
||||||
s = sorted(json.load(sys.stdin) or [], key=t)
|
|
||||||
print(" ".join(x["id"] for x in s[:-1]))')
|
|
||||||
if [[ -n "${ANCIENS}" ]]; then
|
|
||||||
read -r -a ANCIENS_IDS <<< "${ANCIENS}"
|
|
||||||
restic tag --remove "{{ client_backup_etiquette_avant_raser }}" "${ANCIENS_IDS[@]}"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 4. Rétention.
|
# 4. Rétention.
|
||||||
restic forget {{ client_backup_retention }} --keep-tag "{{ client_backup_etiquette_avant_raser }}" --prune
|
restic forget {{ client_backup_retention }} --prune
|
||||||
|
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
# Géré par Set-OPS (rôle client_backup). Ne pas éditer à la main.
|
|
||||||
# Le depot d'avant rasage : etiquete, garde jusqu'a la reconstruction suivante.
|
|
||||||
# Lance par `make sauvegarder-maintenant` (playbooks/backup/restauration.yml), jamais par un minuteur.
|
|
||||||
[Unit]
|
|
||||||
Description=Sauvegarde Set-OPS d'avant rasage (restic hors-nœud, etiquetee)
|
|
||||||
After=network-online.target
|
|
||||||
Wants=network-online.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=oneshot
|
|
||||||
ExecStart=/usr/local/sbin/setops-sauvegarder.sh --avant-raser
|
|
||||||
Nice=10
|
|
||||||
IOSchedulingClass=idle
|
|
||||||
|
|
@ -14,12 +14,6 @@
|
||||||
set -uo pipefail
|
set -uo pipefail
|
||||||
|
|
||||||
export RESTIC_PASSWORD_FILE="/etc/setops/restic.pass"
|
export RESTIC_PASSWORD_FILE="/etc/setops/restic.pass"
|
||||||
# UN VERROU N'EST PAS UNE PANNE (2026-10-04). La sauvegarde, la verification du depot, le
|
|
||||||
# controle de restauration et `setops-restaurer` partagent ce depot ; leurs minuteurs peuvent
|
|
||||||
# partir ensemble (`Persistent=true` rattrape au demarrage tout ce qui a ete manque). Sans
|
|
||||||
# attente, restic refuse aussitot un depot verrouille, et une sonde concluait « aucun
|
|
||||||
# instantane » ou « depot corrompu ». On attend le verrou au lieu de conclure.
|
|
||||||
restic() { command restic --retry-lock {{ client_backup_attente_verrou }} "$@"; }
|
|
||||||
export RESTIC_REPOSITORY="{{ client_backup_repo }}"
|
export RESTIC_REPOSITORY="{{ client_backup_repo }}"
|
||||||
API="https://{{ client_backup_icinga_hote }}.{{ domaine_interne }}:5665"
|
API="https://{{ client_backup_icinga_hote }}.{{ domaine_interne }}:5665"
|
||||||
TTL={{ client_backup_ttl_icinga }}
|
TTL={{ client_backup_ttl_icinga }}
|
||||||
|
|
@ -28,16 +22,6 @@ AGE_CRIT={{ client_backup_age_crit_h }}
|
||||||
MOI="{{ inventory_hostname }}"
|
MOI="{{ inventory_hostname }}"
|
||||||
MOTDEPASSE="$(cat /etc/setops/icinga-api.pass)"
|
MOTDEPASSE="$(cat /etc/setops/icinga-api.pass)"
|
||||||
|
|
||||||
# Les chemins que `setops-sauvegarde` emporte : de quoi dire, devant un instantane vide,
|
|
||||||
# s'il n'y avait RIEN a emporter ou si la sauvegarde a manque des donnees bien presentes.
|
|
||||||
CHEMINS=(
|
|
||||||
{% for job in client_backup_jobs %}
|
|
||||||
{% for c in job.chemins %}
|
|
||||||
"{{ c }}"
|
|
||||||
{% endfor %}
|
|
||||||
{% endfor %}
|
|
||||||
)
|
|
||||||
|
|
||||||
rapporter() { # $1=code $2=texte
|
rapporter() { # $1=code $2=texte
|
||||||
local charge reponse
|
local charge reponse
|
||||||
charge=$(python3 -c 'import json,sys; print(json.dumps({
|
charge=$(python3 -c 'import json,sys; print(json.dumps({
|
||||||
|
|
@ -56,17 +40,9 @@ rapporter() { # $1=code $2=texte
|
||||||
|
|
||||||
maintenant=$(date +%s)
|
maintenant=$(date +%s)
|
||||||
|
|
||||||
# LE PLUS RECENT DE TOUS LES GROUPES, PAS LE PREMIER (2026-10-08). `--latest 1` rend le
|
|
||||||
# dernier instantane de CHAQUE groupe — machine ET liste de chemins. Ajouter un jeu change la
|
|
||||||
# liste : deux groupes, et `d[0]` etait l'ancien, fige. Sur site-mon-01, des le lendemain de
|
|
||||||
# l'arrivee de prometheus, loki et icinga : « EN RETARD : 26 h » sur une sauvegarde de 2 h 30,
|
|
||||||
# et « PERIME » promis a 50 h. On prend donc le plus recent de tous.
|
|
||||||
json=$(restic snapshots --latest 1 --json 2>/dev/null)
|
json=$(restic snapshots --latest 1 --json 2>/dev/null)
|
||||||
horodatage=$(printf '%s' "${json}" | python3 -c \
|
horodatage=$(printf '%s' "${json}" | python3 -c \
|
||||||
'import sys,json,re,datetime
|
'import sys,json;d=json.load(sys.stdin);print(d[0]["time"] if d else "")' 2>/dev/null)
|
||||||
d=json.load(sys.stdin) or []
|
|
||||||
t=lambda x: datetime.datetime.fromisoformat(re.sub(r"Z$","+00:00",re.sub(r"(\.\d{6})\d+",r"\1",x["time"]))).timestamp()
|
|
||||||
print(max(d,key=t)["time"] if d else "")' 2>/dev/null)
|
|
||||||
if [[ -z "${horodatage}" ]]; then
|
if [[ -z "${horodatage}" ]]; then
|
||||||
# ON NE DISTINGUE PAS ICI « depot vide » de « depot injoignable », et c'est voulu :
|
# ON NE DISTINGUE PAS ICI « depot vide » de « depot injoignable », et c'est voulu :
|
||||||
# dans les deux cas il n'y a PAS de sauvegarde exploitable, et c'est ce qui compte.
|
# dans les deux cas il n'y a PAS de sauvegarde exploitable, et c'est ce qui compte.
|
||||||
|
|
@ -92,27 +68,10 @@ for l in sys.stdin:
|
||||||
print(n)' 2>/dev/null || echo 0)
|
print(n)' 2>/dev/null || echo 0)
|
||||||
|
|
||||||
if (( fichiers == 0 )); then
|
if (( fichiers == 0 )); then
|
||||||
# INSTANTANE VIDE : on regarde ce que les chemins sauvegardes contiennent ICI, sur le
|
# AVERTISSEMENT et non CRITIQUE : la machine ne peut pas distinguer « les donnees ont
|
||||||
# noeud. Trois cas, trois messages (2026-10-04) :
|
# disparu » de « il n'y en a pas encore » (un /srv/web sans webapp). C'est a un humain
|
||||||
# - rien sur le noeud : rien a sauvegarder, l'instantane vide est juste. Avertissement
|
# de trancher — mais il doit le VOIR.
|
||||||
# quand meme : un humain doit savoir que ce noeud n'est protege de rien ;
|
rapporter 1 "N'EMPORTE RIEN : instantane sans aucun fichier (${taille} octets). Legitime si ce noeud n'a pas encore de donnees — a confirmer."
|
||||||
# - des fichiers posterieurs a l'instantane : ils sont arrives depuis, la prochaine
|
|
||||||
# sauvegarde les emportera. Avertissement ;
|
|
||||||
# - des fichiers deja la AU MOMENT de l'instantane : la sauvegarde les a manques.
|
|
||||||
# CRITIQUE — c'est une vraie panne.
|
|
||||||
sur_place=0; anciens=0
|
|
||||||
for c in "${CHEMINS[@]}"; do
|
|
||||||
[[ -e "$c" ]] || continue
|
|
||||||
sur_place=$(( sur_place + $(find "$c" -type f 2>/dev/null | wc -l) ))
|
|
||||||
anciens=$(( anciens + $(find "$c" -type f ! -newermt "@${secondes}" 2>/dev/null | wc -l) ))
|
|
||||||
done
|
|
||||||
if (( sur_place == 0 )); then
|
|
||||||
rapporter 1 "RIEN A SAUVEGARDER : ${CHEMINS[*]} ne contient aucun fichier sur ce noeud ; instantane vide, comme attendu."
|
|
||||||
elif (( anciens == 0 )); then
|
|
||||||
rapporter 1 "DONNEES ARRIVEES DEPUIS : ${sur_place} fichier(s) dans ${CHEMINS[*]}, tous posterieurs au dernier instantane (il y a ${age_h} h, vide). La prochaine sauvegarde les emportera."
|
|
||||||
else
|
|
||||||
rapporter 2 "N'EMPORTE RIEN : instantane vide alors que ${CHEMINS[*]} contenait deja ${anciens} fichier(s) a sa prise. La sauvegarde manque ses donnees."
|
|
||||||
fi
|
|
||||||
elif (( age_h >= AGE_CRIT )); then
|
elif (( age_h >= AGE_CRIT )); then
|
||||||
rapporter 2 "PERIME : dernier instantane il y a ${age_h} h (seuil ${AGE_CRIT} h), ${fichiers} fichier(s)."
|
rapporter 2 "PERIME : dernier instantane il y a ${age_h} h (seuil ${AGE_CRIT} h), ${fichiers} fichier(s)."
|
||||||
elif (( age_h >= AGE_WARN )); then
|
elif (( age_h >= AGE_WARN )); then
|
||||||
|
|
|
||||||
|
|
@ -19,12 +19,6 @@
|
||||||
set -uo pipefail
|
set -uo pipefail
|
||||||
|
|
||||||
export RESTIC_PASSWORD_FILE="/etc/setops/restic.pass"
|
export RESTIC_PASSWORD_FILE="/etc/setops/restic.pass"
|
||||||
# UN VERROU N'EST PAS UNE PANNE (2026-10-04). La sauvegarde, la verification du depot, le
|
|
||||||
# controle de restauration et `setops-restaurer` partagent ce depot ; leurs minuteurs peuvent
|
|
||||||
# partir ensemble (`Persistent=true` rattrape au demarrage tout ce qui a ete manque). Sans
|
|
||||||
# attente, restic refuse aussitot un depot verrouille, et une sonde concluait « aucun
|
|
||||||
# instantane » ou « depot corrompu ». On attend le verrou au lieu de conclure.
|
|
||||||
restic() { command restic --retry-lock {{ client_backup_attente_verrou }} "$@"; }
|
|
||||||
export RESTIC_REPOSITORY="{{ client_backup_repo }}"
|
export RESTIC_REPOSITORY="{{ client_backup_repo }}"
|
||||||
API="https://{{ client_backup_icinga_hote }}.{{ domaine_interne }}:5665"
|
API="https://{{ client_backup_icinga_hote }}.{{ domaine_interne }}:5665"
|
||||||
TTL={{ client_backup_ttl_restauration }}
|
TTL={{ client_backup_ttl_restauration }}
|
||||||
|
|
@ -32,15 +26,6 @@ PART="{{ client_backup_restauration_part }}"
|
||||||
MOI="{{ inventory_hostname }}"
|
MOI="{{ inventory_hostname }}"
|
||||||
MOTDEPASSE="$(cat /etc/setops/icinga-api.pass)"
|
MOTDEPASSE="$(cat /etc/setops/icinga-api.pass)"
|
||||||
|
|
||||||
# Les chemins que `setops-sauvegarde` emporte (voir `setops-verifier-mon-depot.sh`).
|
|
||||||
CHEMINS=(
|
|
||||||
{% for job in client_backup_jobs %}
|
|
||||||
{% for c in job.chemins %}
|
|
||||||
"{{ c }}"
|
|
||||||
{% endfor %}
|
|
||||||
{% endfor %}
|
|
||||||
)
|
|
||||||
|
|
||||||
rapporter() { # $1=code $2=texte
|
rapporter() { # $1=code $2=texte
|
||||||
local charge reponse
|
local charge reponse
|
||||||
charge=$(python3 -c 'import json,sys; print(json.dumps({
|
charge=$(python3 -c 'import json,sys; print(json.dumps({
|
||||||
|
|
@ -91,17 +76,7 @@ octets=$(du -sb "${CIBLE}" | cut -f1)
|
||||||
if (( obtenus != attendus )); then
|
if (( obtenus != attendus )); then
|
||||||
rapporter 2 "RESTAURATION INCOMPLETE : ${obtenus} fichier(s) restaure(s) sur ${attendus} dans l'instantane."
|
rapporter 2 "RESTAURATION INCOMPLETE : ${obtenus} fichier(s) restaure(s) sur ${attendus} dans l'instantane."
|
||||||
elif (( attendus == 0 )); then
|
elif (( attendus == 0 )); then
|
||||||
# Avertissement dans les deux cas ; le message dit lequel. Le jugement d'une sauvegarde
|
rapporter 1 "Instantane restaurable mais VIDE : rien a restaurer. Legitime si ce noeud n'a pas encore de donnees."
|
||||||
# qui manque ses donnees revient a la sonde `sauvegarde`, qui sait dater l'instantane.
|
|
||||||
sur_place=0
|
|
||||||
for c in "${CHEMINS[@]}"; do
|
|
||||||
[[ -e "$c" ]] && sur_place=$(( sur_place + $(find "$c" -type f 2>/dev/null | wc -l) ))
|
|
||||||
done
|
|
||||||
if (( sur_place == 0 )); then
|
|
||||||
rapporter 1 "RIEN A RESTAURER : instantane vide, et ${CHEMINS[*]} ne contient aucun fichier sur ce noeud — comme attendu."
|
|
||||||
else
|
|
||||||
rapporter 1 "Instantane restaurable mais VIDE, alors que ${CHEMINS[*]} contient ${sur_place} fichier(s) sur ce noeud : voir la sonde sauvegarde."
|
|
||||||
fi
|
|
||||||
else
|
else
|
||||||
rapporter 0 "Restauration verifiee : ${obtenus} fichier(s), ${octets} octets, contenu recompare ; integrite du depot relue sur ${PART}."
|
rapporter 0 "Restauration verifiee : ${obtenus} fichier(s), ${octets} octets, contenu recompare ; integrite du depot relue sur ${PART}."
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,5 @@ client_backup_groupes_etat:
|
||||||
- serveur_forgejo
|
- serveur_forgejo
|
||||||
- serveur_nextcloud
|
- serveur_nextcloud
|
||||||
- serveur_rspamd
|
- serveur_rspamd
|
||||||
|
- serveur_web_frontal
|
||||||
- serveur_web_dorsal
|
- serveur_web_dorsal
|
||||||
- serveur_icinga
|
|
||||||
- serveur_prometheus
|
|
||||||
- serveur_loki
|
|
||||||
|
|
|
||||||
|
|
@ -88,11 +88,3 @@ client_journal_syslog_emetteur: "frontiere"
|
||||||
# une liste de plus qui suit une autre — et qui prendrait du retard.
|
# une liste de plus qui suit une autre — et qui prendrait du retard.
|
||||||
client_journal_audit_actif: "{{ 'serveur_durci' in group_names }}"
|
client_journal_audit_actif: "{{ 'serveur_durci' in group_names }}"
|
||||||
client_journal_audit_chemin: "/var/log/audit/audit.log"
|
client_journal_audit_chemin: "/var/log/audit/audit.log"
|
||||||
|
|
||||||
# LES MACHINES QUI SONDENT — celles qui portent `client_sante`, donc qui jouent la sonde
|
|
||||||
# « connectivite » chaque minute. Leurs connexions de sonde ne sont pas expediees (voir le
|
|
||||||
# gabarit). Derive de l'inventaire : une machine ajoutee y entre sans qu'on l'ecrive.
|
|
||||||
# Vide = aucun filtre.
|
|
||||||
client_journal_sondeurs: >-
|
|
||||||
{{ groups.get('client_sante', []) | map('extract', hostvars)
|
|
||||||
| map(attribute='ansible_host', default='') | select('match', '^[0-9.]+$') | list }}
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@
|
||||||
# precedent : ce qui AUGMENTE est une perte en cours, ce qui stagne est une cicatrice.
|
# precedent : ce qui AUGMENTE est une perte en cours, ce qui stagne est une cicatrice.
|
||||||
sondes:
|
sondes:
|
||||||
- nom: journaux
|
- nom: journaux
|
||||||
metier: socle
|
|
||||||
ttl: 5400
|
ttl: 5400
|
||||||
raison: >-
|
raison: >-
|
||||||
Les journaux de cette machine partent-ils vraiment vers Loki ? Alloy peut tourner,
|
Les journaux de cette machine partent-ils vraiment vers Loki ? Alloy peut tourner,
|
||||||
|
|
|
||||||
|
|
@ -225,16 +225,11 @@
|
||||||
# perdre ferait passer une cicatrice ancienne pour une perte en cours.
|
# perdre ferait passer une cicatrice ancienne pour une perte en cours.
|
||||||
- name: Assurer le repertoire d etat des sondes
|
- name: Assurer le repertoire d etat des sondes
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
# 0755, COMME PARTOUT AILLEURS (2026-10-01) : quatre roles tenaient ce repertoire, trois
|
|
||||||
# en 0750 et `serveur_artefacts` en 0755 — apt-cacher-ng doit le traverser pour servir
|
|
||||||
# le depot de binaires du site. Le dernier deploye gagnait : au site, le 2026-10-01, il
|
|
||||||
# rendait 403, et chaque runner allait chercher 500 Mo sur Internet. Le repertoire ne
|
|
||||||
# tient que des etats de sondes, deja lisibles. Garde : test_repertoires_partages.py.
|
|
||||||
path: /var/lib/setops
|
path: /var/lib/setops
|
||||||
state: directory
|
state: directory
|
||||||
owner: root
|
owner: root
|
||||||
group: root
|
group: root
|
||||||
mode: "0755"
|
mode: "0750"
|
||||||
|
|
||||||
- name: Deposer la sonde d expedition des journaux
|
- name: Deposer la sonde d expedition des journaux
|
||||||
ansible.builtin.template:
|
ansible.builtin.template:
|
||||||
|
|
|
||||||
|
|
@ -11,85 +11,8 @@ loki.write "loki" {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
{% set sondeurs = client_journal_sondeurs | map('regex_escape') | join('|') | replace('\\', '\\\\') %}
|
|
||||||
{% if sondeurs %}
|
|
||||||
// --- LE BRUIT DE LA SONDE « connectivite » RESTE SUR LA MACHINE (2026-10-03) ---
|
|
||||||
//
|
|
||||||
// Chaque minute, chaque machine ouvre une connexion TCP vers les ports que le registre des
|
|
||||||
// flux lui promet, puis la referme sans rien dire (`nftables_baseline`,
|
|
||||||
// `sonde-connectivite.sh`). Ceux qui ecoutent le notent : `sshd` (« Connection closed by
|
|
||||||
// <pair> port <n> ») et les serveurs Go — Forgejo, step-ca — (« TLS handshake error from
|
|
||||||
// <pair>:<n>: EOF »). Mesure au site : le journal de chaque VM est passe de 2 000 a
|
|
||||||
// 14 000 lignes par jour le 2026-10-01, et ces deux phrases faisaient 98 % des « erreurs »
|
|
||||||
// de la forge et de l'autorite. Rien a corriger a la source : `sshd` journalise toute
|
|
||||||
// connexion fermee avant l'echange de cles, quel que soit le client.
|
|
||||||
//
|
|
||||||
// CE QUI EST JETE EST ETROIT : la phrase EXACTE, et seulement depuis une machine qui porte
|
|
||||||
// la sonde (`client_sante`, derive de l'inventaire). Le meme message venu d'ailleurs — un
|
|
||||||
// balayage, une machine inconnue — passe. Les lignes restent dans le journal local ; le
|
|
||||||
// compte de ce qui est jete est `loki_process_dropped_lines_total{reason="sonde_connectivite"}`,
|
|
||||||
// distinct des pertes que surveille la sonde « journaux ».
|
|
||||||
loki.process "sondes" {
|
|
||||||
forward_to = [loki.write.loki.receiver]
|
|
||||||
|
|
||||||
stage.drop {
|
|
||||||
expression = "^Connection closed by ({{ sondeurs }}) port [0-9]+$"
|
|
||||||
drop_counter_reason = "sonde_connectivite"
|
|
||||||
}
|
|
||||||
|
|
||||||
stage.drop {
|
|
||||||
expression = "TLS handshake error from ({{ sondeurs }}):[0-9]+: EOF$"
|
|
||||||
drop_counter_reason = "sonde_connectivite"
|
|
||||||
}
|
|
||||||
|
|
||||||
// LE COURRIEL PARLE PLUS QUE LES AUTRES (mesure chez les deux locataires, 2026-10-03).
|
|
||||||
// Postfix ecrit trois lignes par connexion de sonde — quatre sur le port chiffre — et
|
|
||||||
// faisait de `edge-mta-01` la machine la plus bavarde de chaque locataire (7 000 lignes
|
|
||||||
// par heure). Une VRAIE session venue de la flotte perd sa ligne `connect from`, rien
|
|
||||||
// d'autre : ses identifiants de file (`client=`), et son `disconnect` qui compte des
|
|
||||||
// commandes (`commands=N/M`, jamais `0/0`), restent.
|
|
||||||
stage.drop {
|
|
||||||
expression = "^connect from [^\\[ ]+\\[({{ sondeurs }})\\]$"
|
|
||||||
drop_counter_reason = "sonde_connectivite"
|
|
||||||
}
|
|
||||||
|
|
||||||
stage.drop {
|
|
||||||
expression = "^(NOQUEUE: )?lost connection after CONNECT from [^\\[ ]+\\[({{ sondeurs }})\\]$"
|
|
||||||
drop_counter_reason = "sonde_connectivite"
|
|
||||||
}
|
|
||||||
|
|
||||||
stage.drop {
|
|
||||||
expression = "^disconnect from [^\\[ ]+\\[({{ sondeurs }})\\] commands=0/0$"
|
|
||||||
drop_counter_reason = "sonde_connectivite"
|
|
||||||
}
|
|
||||||
|
|
||||||
stage.drop {
|
|
||||||
expression = "^SSL_accept error from [^\\[ ]+\\[({{ sondeurs }})\\]: lost connection$"
|
|
||||||
drop_counter_reason = "sonde_connectivite"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dovecot LMTP, en TLS des l'accueil : la sonde ferme avant la poignee. Seule la forme
|
|
||||||
// « fermee avant de commencer » est jetee ; toute autre raison d'echec TLS passe, et une
|
|
||||||
// livraison reelle se lit toujours a `saved mail to`.
|
|
||||||
stage.drop {
|
|
||||||
expression = "^lmtp\\([0-9]+\\): Connect from ({{ sondeurs }})$"
|
|
||||||
drop_counter_reason = "sonde_connectivite"
|
|
||||||
}
|
|
||||||
|
|
||||||
stage.drop {
|
|
||||||
expression = "^lmtp\\([0-9]+\\): Error: lmtp-server: conn ({{ sondeurs }}):[0-9]+ \\[[0-9]+\\]: SSL handshake failed: Connection closed$"
|
|
||||||
drop_counter_reason = "sonde_connectivite"
|
|
||||||
}
|
|
||||||
|
|
||||||
stage.drop {
|
|
||||||
expression = "^lmtp\\([0-9]+\\): Disconnect from ({{ sondeurs }}): SSL Initialization failed \\(state=GREETING\\)$"
|
|
||||||
drop_counter_reason = "sonde_connectivite"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
{% endif %}
|
|
||||||
loki.source.journal "journal" {
|
loki.source.journal "journal" {
|
||||||
forward_to = [{{ 'loki.process.sondes.receiver' if sondeurs else 'loki.write.loki.receiver' }}]
|
forward_to = [loki.write.loki.receiver]
|
||||||
max_age = "12h"
|
max_age = "12h"
|
||||||
labels = {
|
labels = {
|
||||||
job = "systemd-journal",
|
job = "systemd-journal",
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@
|
||||||
# donc DECLARES sans objet, dans une liste qu'une machine de metal peut vider.
|
# donc DECLARES sans objet, dans une liste qu'une machine de metal peut vider.
|
||||||
sondes:
|
sondes:
|
||||||
- nom: metriques
|
- nom: metriques
|
||||||
metier: socle
|
|
||||||
# Six fois la periode du porteur (15 min) : cinq passages peuvent manquer avant
|
# Six fois la periode du porteur (15 min) : cinq passages peuvent manquer avant
|
||||||
# qu'Icinga ne perime. On ne crie pas pour un hoquet, on crie pour un silence.
|
# qu'Icinga ne perime. On ne crie pas pour un hoquet, on crie pour un silence.
|
||||||
ttl: 5400
|
ttl: 5400
|
||||||
|
|
|
||||||
|
|
@ -43,16 +43,6 @@ client_pki_cle: "{{ client_pki_steppath }}/certs/{{ client_pki_nom_cert }}.key"
|
||||||
# ON N'AJOUTE QUE CE QUE CET HOTE SERT REELLEMENT : `edge` nomme le groupe qui rend le
|
# ON N'AJOUTE QUE CE QUE CET HOTE SERT REELLEMENT : `edge` nomme le groupe qui rend le
|
||||||
# service, et l'hote doit en faire partie. Un certificat qui revendiquerait le nom d'un
|
# service, et l'hote doit en faire partie. Un certificat qui revendiquerait le nom d'un
|
||||||
# service rendu ailleurs serait une usurpation, pas une commodite.
|
# service rendu ailleurs serait une usurpation, pas une commodite.
|
||||||
#
|
|
||||||
# L'AC INTERNE NE SIGNE QUE LE DOMAINE INTERNE (2026-09-29). Le web frontal sert les noms
|
|
||||||
# PUBLICS du locataire : ils relevent de Let's Encrypt. step-ca n'a aucune politique de noms
|
|
||||||
# et les aurait signes — sans danger (personne dehors ne lui fait confiance), mais sans objet.
|
|
||||||
#
|
|
||||||
# ET LE SERVICE LUI-MEME, QUAND LES MACHINES Y VONT DIRECTEMENT (2026-09-29). Avec
|
|
||||||
# `resolution_interne: service` (le site), le plancher et la zone menent le nom au service,
|
|
||||||
# pas a l'edge : c'est lui qui doit le porter. Sans cela, une reconstruction de
|
|
||||||
# `site-forge-01` aurait rendu un certificat sans `forge.genese.internal`, et le runner du
|
|
||||||
# site — qui y va directement — aurait bute sur le nom.
|
|
||||||
client_pki_expositions: "{{ hosts_statiques_expositions | default([]) }}"
|
client_pki_expositions: "{{ hosts_statiques_expositions | default([]) }}"
|
||||||
client_pki_sans: >-
|
client_pki_sans: >-
|
||||||
{{ ([client_pki_nom_cert,
|
{{ ([client_pki_nom_cert,
|
||||||
|
|
@ -62,14 +52,6 @@ client_pki_sans: >-
|
||||||
| selectattr('edge', 'defined')
|
| selectattr('edge', 'defined')
|
||||||
| selectattr('fqdn', 'defined')
|
| selectattr('fqdn', 'defined')
|
||||||
| selectattr('edge', 'in', group_names)
|
| selectattr('edge', 'in', group_names)
|
||||||
| selectattr('domaine', 'defined')
|
|
||||||
| selectattr('domaine', 'equalto', domaine_interne | default(''))
|
|
||||||
| map(attribute='fqdn') | list)
|
|
||||||
+ (client_pki_expositions
|
|
||||||
| selectattr('fqdn', 'defined')
|
|
||||||
| selectattr('resout_vers', 'defined')
|
|
||||||
| selectattr('resout_vers', 'equalto', 'service')
|
|
||||||
| selectattr('hote', 'equalto', inventory_hostname)
|
|
||||||
| map(attribute='fqdn') | list))
|
| map(attribute='fqdn') | list))
|
||||||
| select | unique | list }}
|
| select | unique | list }}
|
||||||
|
|
||||||
|
|
@ -85,6 +67,21 @@ client_pki_provisioner_password: "{{ vault_step_ca_provisioner_password | defaul
|
||||||
# Depot apt officiel Smallstep (partage avec serveur_step_ca).
|
# Depot apt officiel Smallstep (partage avec serveur_step_ca).
|
||||||
client_pki_depot_cle_url: "{{ client_pki_depot_schema }}://packages.smallstep.com/keys/apt/repo-signing-key.gpg"
|
client_pki_depot_cle_url: "{{ client_pki_depot_schema }}://packages.smallstep.com/keys/apt/repo-signing-key.gpg"
|
||||||
client_pki_depot_cle_fichier: "/etc/apt/keyrings/smallstep.asc"
|
client_pki_depot_cle_fichier: "/etc/apt/keyrings/smallstep.asc"
|
||||||
|
|
||||||
|
# LA MEME CLE QUE `serveur_step_ca` POSE, et c'est le DEPOT qui la porte : les deux
|
||||||
|
# fichiers ont le meme SHA-256. La chercher ici d'abord evite de sortir sur un CDN tiers
|
||||||
|
# pour des octets qu'on possede.
|
||||||
|
#
|
||||||
|
# CE N'EST PAS THEORIQUE POUR UNE FLOTTE D'AMORCAGE. Elle n'a par definition ni serveur
|
||||||
|
# d'artefacts ni cache, donc `client_pki_depot_schema` reste `https` et chaque hote sort
|
||||||
|
# EN DIRECT ; le telechargement echouait alors trois amorcages sur quatre, avec cinq
|
||||||
|
# reprises, sur une bordure amont que rien ici ne controle.
|
||||||
|
#
|
||||||
|
# Le chemin traverse `serveur_step_ca` plutot que de dupliquer le fichier : deux copies
|
||||||
|
# d'une meme cle divergent le jour ou l'amont la remplace, et rien ne dirait laquelle
|
||||||
|
# fait foi.
|
||||||
|
client_pki_depot_cle_locale: >-
|
||||||
|
{{ role_path }}/../serveur_step_ca/files/smallstep-repo-signing-key.asc
|
||||||
client_pki_depot_uri: "{{ client_pki_depot_schema }}://packages.smallstep.com/stable/debian"
|
client_pki_depot_uri: "{{ client_pki_depot_schema }}://packages.smallstep.com/stable/debian"
|
||||||
client_pki_depot_suite: "debs"
|
client_pki_depot_suite: "debs"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,6 @@
|
||||||
# contraire. La sonde compare donc les deux quand un service consomme le certificat.
|
# contraire. La sonde compare donc les deux quand un service consomme le certificat.
|
||||||
sondes:
|
sondes:
|
||||||
- nom: certificat
|
- nom: certificat
|
||||||
metier: socle
|
|
||||||
# 5400 s = une heure et demie, soit six fois la periode du porteur (15 min). Cinq
|
# 5400 s = une heure et demie, soit six fois la periode du porteur (15 min). Cinq
|
||||||
# passages peuvent manquer avant qu'Icinga ne perime : on ne crie pas pour un hoquet,
|
# passages peuvent manquer avant qu'Icinga ne perime : on ne crie pas pour un hoquet,
|
||||||
# on crie pour un silence.
|
# on crie pour un silence.
|
||||||
|
|
|
||||||
|
|
@ -20,17 +20,10 @@
|
||||||
# L'empreinte du root CA est la SOURCE DE VÉRITÉ de l'autorité elle-même : on la
|
# L'empreinte du root CA est la SOURCE DE VÉRITÉ de l'autorité elle-même : on la
|
||||||
# dérive à chaud (robuste au from-zero — une AC régénérée a une empreinte neuve).
|
# dérive à chaud (robuste au from-zero — une AC régénérée a une empreinte neuve).
|
||||||
# `client_pki_ca_fingerprint_override` permet d'épingler explicitement si besoin.
|
# `client_pki_ca_fingerprint_override` permet d'épingler explicitement si besoin.
|
||||||
#
|
|
||||||
# UNE FOIS POUR TOUTE LA FLOTTE (`run_once`, 2026-09-30). L'empreinte est la meme pour
|
|
||||||
# chaque hote : la deriver par hote lancait TREIZE modules Python en parallele sur l'AC —
|
|
||||||
# 765 Mo, 1 vCPU. A la reconstruction de Technolibre, le noyau a tue le module de
|
|
||||||
# `collab-01` (rc=137) et l'agent de journaux au passage. Avec `run_once`, le resultat
|
|
||||||
# enregistre vaut pour tous les hotes du play.
|
|
||||||
- name: Dériver l'empreinte du root CA depuis l'autorité
|
- name: Dériver l'empreinte du root CA depuis l'autorité
|
||||||
ansible.builtin.command:
|
ansible.builtin.command:
|
||||||
cmd: "step certificate fingerprint {{ serveur_step_ca_steppath | default('/etc/step-ca') }}/certs/root_ca.crt"
|
cmd: "step certificate fingerprint {{ serveur_step_ca_steppath | default('/etc/step-ca') }}/certs/root_ca.crt"
|
||||||
delegate_to: "{{ groups['serveur_step_ca'][0] }}"
|
delegate_to: "{{ groups['serveur_step_ca'][0] }}"
|
||||||
run_once: true
|
|
||||||
changed_when: false
|
changed_when: false
|
||||||
check_mode: false
|
check_mode: false
|
||||||
register: client_pki_fingerprint_ac
|
register: client_pki_fingerprint_ac
|
||||||
|
|
@ -110,8 +103,36 @@
|
||||||
- telecharger_la_cle_de_signature_smallste_present.stat.exists
|
- telecharger_la_cle_de_signature_smallste_present.stat.exists
|
||||||
- (telecharger_la_cle_de_signature_smallste_present.stat.size | default(0)) == 0
|
- (telecharger_la_cle_de_signature_smallste_present.stat.size | default(0)) == 0
|
||||||
|
|
||||||
|
- name: Voir si le depot porte la cle de signature Smallstep
|
||||||
|
ansible.builtin.stat:
|
||||||
|
path: "{{ client_pki_depot_cle_locale }}"
|
||||||
|
delegate_to: localhost
|
||||||
|
become: false
|
||||||
|
changed_when: false
|
||||||
|
check_mode: false
|
||||||
|
register: client_pki_cle_depot_locale
|
||||||
|
|
||||||
|
# LE DEPOT D'ABORD, LE RESEAU ENSUITE. `serveur_step_ca` pose deja cette cle depuis ses
|
||||||
|
# `files/` sans rien demander a personne ; le client la telechargeait. Les deux fichiers
|
||||||
|
# sont identiques — meme SHA-256 — si bien que la sortie n'achetait rien et exposait
|
||||||
|
# chaque amorcage a une bordure tierce.
|
||||||
|
- name: Poser la cle de signature Smallstep depuis le depot
|
||||||
|
ansible.builtin.copy:
|
||||||
|
src: "{{ client_pki_depot_cle_locale }}"
|
||||||
|
dest: "{{ client_pki_depot_cle_fichier }}"
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: "0644"
|
||||||
|
when:
|
||||||
|
- not (telecharger_la_cle_de_signature_smallste_present.stat.exists and telecharger_la_cle_de_signature_smallste_present.stat.size | default(0) > 0)
|
||||||
|
- client_pki_cle_depot_locale.stat.exists
|
||||||
|
|
||||||
|
# LE REPLI RESTE, et il n'est pas mort : un depot qui ne porterait plus ce fichier doit
|
||||||
|
# encore pouvoir amorcer. Ce qui change est l'ORDRE — on ne sort que faute de mieux.
|
||||||
- name: Telecharger la cle de signature Smallstep
|
- name: Telecharger la cle de signature Smallstep
|
||||||
when: not (telecharger_la_cle_de_signature_smallste_present.stat.exists and telecharger_la_cle_de_signature_smallste_present.stat.size | default(0) > 0)
|
when:
|
||||||
|
- not (telecharger_la_cle_de_signature_smallste_present.stat.exists and telecharger_la_cle_de_signature_smallste_present.stat.size | default(0) > 0)
|
||||||
|
- not client_pki_cle_depot_locale.stat.exists
|
||||||
# `force` PARCE QU'UNE REPRISE CONDITIONNELLE NE REPREND RIEN (2026-09-12).
|
# `force` PARCE QU'UNE REPRISE CONDITIONNELLE NE REPREND RIEN (2026-09-12).
|
||||||
#
|
#
|
||||||
# Sans lui, `get_url` envoie un `If-Modified-Since` des que le fichier existe. Le cache
|
# Sans lui, `get_url` envoie un `If-Modified-Since` des que le fichier existe. Le cache
|
||||||
|
|
|
||||||
|
|
@ -52,11 +52,6 @@ client_sante_unites_tolerees: []
|
||||||
# Six passages du porteur.
|
# Six passages du porteur.
|
||||||
client_sante_ttl_sondes: 5400
|
client_sante_ttl_sondes: 5400
|
||||||
|
|
||||||
# LES SONDES A LA MINUTE (2026-09-30) — voir `setops-sante.sh.j2`, mode `minute`. Le `ttl`
|
|
||||||
# couvre trois rapports : une minute manquee ne crie pas, trois oui.
|
|
||||||
client_sante_repertoire_minute: /usr/local/lib/setops/sondes-minute
|
|
||||||
client_sante_ttl_minute: 180
|
|
||||||
|
|
||||||
# Delai de garde de CHAQUE sonde. Une sonde est cense repondre vite ; celle qui depasse
|
# Delai de garde de CHAQUE sonde. Une sonde est cense repondre vite ; celle qui depasse
|
||||||
# n'a pas de verdict, et on le dit (INCONNU) plutot que de figer tout le rapport.
|
# n'a pas de verdict, et on le dit (INCONNU) plutot que de figer tout le rapport.
|
||||||
client_sante_delai_sonde: 20
|
client_sante_delai_sonde: 20
|
||||||
|
|
@ -74,15 +69,3 @@ client_sante_paquets:
|
||||||
# machines ; ces seuils laissent donc voir une croissance longtemps avant la panne.
|
# machines ; ces seuils laissent donc voir une croissance longtemps avant la panne.
|
||||||
client_sante_disque_avert: 80
|
client_sante_disque_avert: 80
|
||||||
client_sante_disque_crit: 90
|
client_sante_disque_crit: 90
|
||||||
|
|
||||||
# Le plancher que la sonde « plancher » compare a l'autoritatif. Mise en defaut PAR
|
|
||||||
# PARAMETRE : un fichier vide ou absent — la sonde doit alors voir tous les noms manquer.
|
|
||||||
client_sante_plancher_fichier: "/etc/hosts"
|
|
||||||
|
|
||||||
# LES UNITES QUE CE ROLE POSE — la liste que `tasks/retirer.yml` defait sur un hote sorti du
|
|
||||||
# groupe. Ecrite a cote des gabarits qu'elle nomme : une unite ajoutee au role s'ajoute ici.
|
|
||||||
client_sante_unites_posees:
|
|
||||||
- setops-sante.timer
|
|
||||||
- setops-sante.service
|
|
||||||
- setops-sondes-minute.timer
|
|
||||||
- setops-sondes-minute.service
|
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,3 @@
|
||||||
- name: Recharger systemd
|
- name: Recharger systemd
|
||||||
ansible.builtin.systemd:
|
ansible.builtin.systemd:
|
||||||
daemon_reload: true
|
daemon_reload: true
|
||||||
|
|
||||||
# UNE UNITE RETIREE PEUT RESTER « failed » DANS LA MEMOIRE DE systemd, et c'est cet etat
|
|
||||||
# que lit `node_exporter`. Recharger ne l'efface pas : il faut `reset-failed`, faute de
|
|
||||||
# quoi le retrait se verrait sur disque et pas dans les metriques (`tasks/retirer.yml`).
|
|
||||||
- name: Oublier l'echec du porteur retire
|
|
||||||
ansible.builtin.shell: |
|
|
||||||
systemctl reset-failed setops-sante.service setops-sondes-minute.service 2>/dev/null || true
|
|
||||||
systemctl daemon-reload
|
|
||||||
changed_when: true
|
|
||||||
|
|
|
||||||
|
|
@ -9,20 +9,8 @@
|
||||||
# script, et P64 veut, a juste titre, que la declaration suive le role qui depose.
|
# script, et P64 veut, a juste titre, que la declaration suive le role qui depose.
|
||||||
sondes:
|
sondes:
|
||||||
- nom: disque
|
- nom: disque
|
||||||
metier: socle
|
|
||||||
ttl: 5400
|
ttl: 5400
|
||||||
raison: >-
|
raison: >-
|
||||||
Reste-t-il de la place, et des inodes, sur chaque systeme de fichiers ? Seuls le
|
Reste-t-il de la place, et des inodes, sur chaque systeme de fichiers ? Seuls le
|
||||||
cache apt et les depots de sauvegarde etaient surveilles : un disque plein arretait
|
cache apt et les depots de sauvegarde etaient surveilles : un disque plein arretait
|
||||||
la base ou le courriel sans prevenir.
|
la base ou le courriel sans prevenir.
|
||||||
- nom: plancher
|
|
||||||
metier: socle
|
|
||||||
ttl: 5400
|
|
||||||
# Posee sous la condition du plancher (voir `setops-sondes.conf.j2`, 2026-09-28).
|
|
||||||
seulement_si: hosts_statiques_actif
|
|
||||||
defini_par: hosts_statiques
|
|
||||||
raison: >-
|
|
||||||
Le plancher /etc/hosts de la machine dit-il la meme chose que l'autoritatif ? Les deux
|
|
||||||
suivent le plan, chacun quand son role est rejoue. Le 2026-09-29, la console manquait
|
|
||||||
aux planchers des deux locataires : depuis l'edge, elle ne se resolvait pas, et le
|
|
||||||
plancher y est la seule resolution des noms internes.
|
|
||||||
|
|
|
||||||
|
|
@ -27,14 +27,11 @@
|
||||||
|
|
||||||
- name: Assurer le répertoire des secrets Set-OPS
|
- name: Assurer le répertoire des secrets Set-OPS
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
# 0700, COMME `client_backup` ET `serveur_backup` (2026-10-01) : ce repertoire tient des
|
|
||||||
# secrets, et tous ses lecteurs tournent en root. Ce role etait seul a l'ouvrir en 0755,
|
|
||||||
# et le dernier deploye gagnait. Garde : test_repertoires_partages.py.
|
|
||||||
path: /etc/setops
|
path: /etc/setops
|
||||||
state: directory
|
state: directory
|
||||||
owner: root
|
owner: root
|
||||||
group: root
|
group: root
|
||||||
mode: "0700"
|
mode: "0755"
|
||||||
|
|
||||||
- name: Déposer le mot de passe d'API Icinga
|
- name: Déposer le mot de passe d'API Icinga
|
||||||
ansible.builtin.copy:
|
ansible.builtin.copy:
|
||||||
|
|
@ -166,31 +163,9 @@
|
||||||
group: root
|
group: root
|
||||||
mode: "0750"
|
mode: "0750"
|
||||||
|
|
||||||
# LA SONDE « plancher » (2026-09-29), sous la condition meme du plancher : sans plancher
|
|
||||||
# gere (`hosts_statiques_actif: false`), il n'y a rien a comparer. Posee ici pour la raison
|
|
||||||
# de « disque » : ce role tourne sur tout hote qui rapporte, sans toucher au systeme.
|
|
||||||
- name: Deposer la sonde du plancher de resolution
|
|
||||||
ansible.builtin.template:
|
|
||||||
src: sonde-plancher.sh.j2
|
|
||||||
dest: /usr/local/lib/setops/sondes/plancher.sh
|
|
||||||
owner: root
|
|
||||||
group: root
|
|
||||||
mode: "0750"
|
|
||||||
when: hosts_statiques_actif | default(true) | bool
|
|
||||||
|
|
||||||
- name: Assurer le repertoire des sondes a la minute
|
|
||||||
ansible.builtin.file:
|
|
||||||
path: "{{ client_sante_repertoire_minute }}"
|
|
||||||
state: directory
|
|
||||||
owner: root
|
|
||||||
group: root
|
|
||||||
mode: "0755"
|
|
||||||
|
|
||||||
- name: Relever les sondes reellement posees
|
- name: Relever les sondes reellement posees
|
||||||
ansible.builtin.find:
|
ansible.builtin.find:
|
||||||
paths:
|
paths: "{{ client_sante_repertoire_sondes | default('/usr/local/lib/setops/sondes') }}"
|
||||||
- "{{ client_sante_repertoire_sondes | default('/usr/local/lib/setops/sondes') }}"
|
|
||||||
- "{{ client_sante_repertoire_minute }}"
|
|
||||||
patterns: "*.sh"
|
patterns: "*.sh"
|
||||||
register: client_sante_posees
|
register: client_sante_posees
|
||||||
|
|
||||||
|
|
@ -236,33 +211,11 @@
|
||||||
state: started
|
state: started
|
||||||
daemon_reload: true
|
daemon_reload: true
|
||||||
|
|
||||||
- name: Déployer l'unité et le minuteur des sondes a la minute
|
|
||||||
ansible.builtin.template:
|
|
||||||
src: "{{ item }}.j2"
|
|
||||||
dest: "/etc/systemd/system/{{ item }}"
|
|
||||||
owner: root
|
|
||||||
group: root
|
|
||||||
mode: "0644"
|
|
||||||
loop:
|
|
||||||
- setops-sondes-minute.service
|
|
||||||
- setops-sondes-minute.timer
|
|
||||||
notify: Recharger systemd
|
|
||||||
|
|
||||||
- name: Activer les sondes a la minute
|
|
||||||
ansible.builtin.systemd:
|
|
||||||
name: setops-sondes-minute.timer
|
|
||||||
enabled: true
|
|
||||||
state: started
|
|
||||||
daemon_reload: true
|
|
||||||
# En simulation, l'unite n'a pas ete posee : l'activer echouerait sans rien dire du reel.
|
|
||||||
when: not ansible_check_mode
|
|
||||||
|
|
||||||
# PREMIER RAPPORT IMMEDIAT, et ce n'est pas une commodite : sans lui, le service
|
# PREMIER RAPPORT IMMEDIAT, et ce n'est pas une commodite : sans lui, le service
|
||||||
# reste « en attente » jusqu'au premier declenchement, et un deploiement qui vient
|
# reste « en attente » jusqu'au premier declenchement, et un deploiement qui vient
|
||||||
# d'installer la supervision laisserait un tableau vide qu'on prendrait pour un
|
# d'installer la supervision laisserait un tableau vide qu'on prendrait pour un
|
||||||
# tableau sain.
|
# tableau sain.
|
||||||
- name: Rapporter tout de suite (ne pas laisser un tableau vide passer pour sain)
|
- name: Rapporter tout de suite (ne pas laisser un tableau vide passer pour sain)
|
||||||
ansible.builtin.command: "/usr/local/sbin/setops-sante.sh {{ item }}"
|
ansible.builtin.command: /usr/local/sbin/setops-sante.sh
|
||||||
loop: ["", "minute"]
|
|
||||||
changed_when: false
|
changed_when: false
|
||||||
failed_when: false
|
failed_when: false
|
||||||
|
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
---
|
|
||||||
# DEFAIRE CE QUE CE ROLE A POSE, SUR UN HOTE QUI N'EST PLUS DANS SON GROUPE (2026-10-03).
|
|
||||||
#
|
|
||||||
# Le cas qui l'a fait ecrire : les trois hyperviseurs du site. Ils ont recu `client_sante`
|
|
||||||
# le 2026-09-10, puis en sont sortis le jour meme — la route par defaut gelee (D-57)
|
|
||||||
# empeche un hyperviseur de joindre Icinga, et leurs metriques sont depuis TIREES par
|
|
||||||
# Prometheus (voir `scripts/site_inventaire.py`). Sortis du groupe, plus aucun play ne les
|
|
||||||
# touchait : le minuteur est reste, pointe sur `10.0.36.11`, l'adresse de `site-mon-01`
|
|
||||||
# d'avant la renumerotation.
|
|
||||||
#
|
|
||||||
# Mesure du 2026-10-03 : un echec toutes les 15 min depuis au moins sept jours, environ
|
|
||||||
# 2 050 par hyperviseur. Et `node_exporter` publiait sur chacun UNE unite en echec
|
|
||||||
# permanente — exactement ce que ce role refuse ailleurs (`client_sante_unites_tolerees`) :
|
|
||||||
# le jour ou une vraie unite tombe, le compte passe de 1 a 2 et personne ne le voit.
|
|
||||||
#
|
|
||||||
# ON NE RETIRE QUE CE QUE CE ROLE POSE. Les sondes de `client_journal` et de
|
|
||||||
# `client_metrique` restent dans `/usr/local/lib/setops/sondes/` : ces roles s'appliquent
|
|
||||||
# encore, ils les redeposeraient au prochain passage, et sans porteur elles sont inertes.
|
|
||||||
- name: Les unites du porteur sont-elles encore la ?
|
|
||||||
ansible.builtin.stat:
|
|
||||||
path: "/etc/systemd/system/{{ item }}"
|
|
||||||
loop: "{{ client_sante_unites_posees }}"
|
|
||||||
register: client_sante_unites_restantes
|
|
||||||
|
|
||||||
- name: Arreter et desactiver les minuteurs restes en place
|
|
||||||
ansible.builtin.systemd:
|
|
||||||
name: "{{ item.item }}"
|
|
||||||
state: stopped
|
|
||||||
enabled: false
|
|
||||||
loop: "{{ client_sante_unites_restantes.results }}"
|
|
||||||
loop_control:
|
|
||||||
label: "{{ item.item }}"
|
|
||||||
when:
|
|
||||||
- item.stat.exists
|
|
||||||
- item.item is search('\.timer$')
|
|
||||||
|
|
||||||
- name: Retirer les unites, le script et les secrets du rapport passif
|
|
||||||
ansible.builtin.file:
|
|
||||||
path: "{{ item }}"
|
|
||||||
state: absent
|
|
||||||
loop: >-
|
|
||||||
{{ client_sante_unites_posees | map('regex_replace', '^', '/etc/systemd/system/') | list
|
|
||||||
+ ['/usr/local/sbin/setops-sante.sh',
|
|
||||||
'/etc/setops/icinga-api.pass',
|
|
||||||
client_sante_ca_verification,
|
|
||||||
'/usr/local/lib/setops/sondes/disque.sh',
|
|
||||||
'/usr/local/lib/setops/sondes/plancher.sh'] }}
|
|
||||||
notify: Oublier l'echec du porteur retire
|
|
||||||
|
|
@ -109,9 +109,9 @@ echecs=("${restantes[@]}")
|
||||||
# sortirait en 127 et Icinga recevrait un code hors bornes — ou rien. On rapporte donc
|
# sortirait en 127 et Icinga recevrait un code hors bornes — ou rien. On rapporte donc
|
||||||
# CRITIQUE, en le disant : le pire serait qu'un script mort produise un service muet, que
|
# CRITIQUE, en le disant : le pire serait qu'un script mort produise un service muet, que
|
||||||
# le `ttl` finirait par perimer sans que personne sache pourquoi.
|
# le `ttl` finirait par perimer sans que personne sache pourquoi.
|
||||||
sondes() { # $1=repertoire $2=ttl des resultats
|
sondes() {
|
||||||
local f nom sortie code
|
local f nom sortie code
|
||||||
for f in "$1"/*.sh; do
|
for f in /usr/local/lib/setops/sondes/*.sh; do
|
||||||
[[ -x "$f" ]] || continue
|
[[ -x "$f" ]] || continue
|
||||||
nom=$(basename "$f" .sh)
|
nom=$(basename "$f" .sh)
|
||||||
# DELAI DE GARDE SUR CHAQUE SONDE — mesure du 2026-09-09.
|
# DELAI DE GARDE SUR CHAQUE SONDE — mesure du 2026-09-09.
|
||||||
|
|
@ -125,31 +125,21 @@ sondes() { # $1=repertoire $2=ttl des resultats
|
||||||
# INCONNU (3) en le disant, plutot que de laisser le porteur mourir en silence.
|
# INCONNU (3) en le disant, plutot que de laisser le porteur mourir en silence.
|
||||||
sortie=$(timeout {{ client_sante_delai_sonde }} "$f" 2>&1); code=$?
|
sortie=$(timeout {{ client_sante_delai_sonde }} "$f" 2>&1); code=$?
|
||||||
if (( code == 124 )); then
|
if (( code == 124 )); then
|
||||||
rapporter "$nom" 3 "Sonde ${nom} sans reponse apres {{ client_sante_delai_sonde }} s — aucun verdict." "$2"
|
rapporter "$nom" 3 "Sonde ${nom} sans reponse apres {{ client_sante_delai_sonde }} s — aucun verdict." "{{ client_sante_ttl_sondes }}"
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
if (( code > 3 )); then
|
if (( code > 3 )); then
|
||||||
rapporter "$nom" 2 "Sonde ${nom} en erreur (code ${code}) : ${sortie:0:160}" "$2"
|
rapporter "$nom" 2 "Sonde ${nom} en erreur (code ${code}) : ${sortie:0:160}" "{{ client_sante_ttl_sondes }}"
|
||||||
else
|
else
|
||||||
rapporter "$nom" "$code" "${sortie:-(aucun message)}" "$2"
|
rapporter "$nom" "$code" "${sortie:-(aucun message)}" "{{ client_sante_ttl_sondes }}"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
# LE MODE MINUTE (2026-09-30). Certaines sondes doivent battre a la minute — `connectivite`,
|
|
||||||
# qui dit qu'un flux est coupe des qu'il l'est. Elles vivent dans leur propre repertoire,
|
|
||||||
# HORS de celui-ci : sinon ce rapport-ci les rejouerait au quart d'heure avec un `ttl` de
|
|
||||||
# 90 min, et leur silence ne se verrait plus avant une heure et demie. Meme rapporteur, meme
|
|
||||||
# compte, aucun second script a tenir aligne.
|
|
||||||
if [[ "${1:-}" == "minute" ]]; then
|
|
||||||
sondes "{{ client_sante_repertoire_minute }}" "{{ client_sante_ttl_minute }}"
|
|
||||||
exit $(( ECHECS_RAPPORT > 0 ? 1 : 0 ))
|
|
||||||
fi
|
|
||||||
|
|
||||||
n=${#echecs[@]}
|
n=${#echecs[@]}
|
||||||
if [[ $n -eq 0 ]]; then
|
if [[ $n -eq 0 ]]; then
|
||||||
rapporter "sante" 0 "Aucune unite systemd en echec." "${TTL}"
|
rapporter "sante" 0 "Aucune unite systemd en echec." "${TTL}"
|
||||||
sondes /usr/local/lib/setops/sondes "{{ client_sante_ttl_sondes }}"
|
sondes
|
||||||
exit $(( ECHECS_RAPPORT > 0 ? 1 : 0 ))
|
exit $(( ECHECS_RAPPORT > 0 ? 1 : 0 ))
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -157,5 +147,5 @@ fi
|
||||||
# probleme, soit du bruit qu'il faut retirer : dans les deux cas il faut agir. Un seuil
|
# probleme, soit du bruit qu'il faut retirer : dans les deux cas il faut agir. Un seuil
|
||||||
# ferait vivre le bruit indefiniment, et c'est exactement ce qu'on vient de corriger.
|
# ferait vivre le bruit indefiniment, et c'est exactement ce qu'on vient de corriger.
|
||||||
rapporter "sante" 2 "$n unite(s) systemd en echec : ${echecs[*]}" "${TTL}"
|
rapporter "sante" 2 "$n unite(s) systemd en echec : ${echecs[*]}" "${TTL}"
|
||||||
sondes /usr/local/lib/setops/sondes "{{ client_sante_ttl_sondes }}"
|
sondes
|
||||||
exit $(( ECHECS_RAPPORT > 0 ? 1 : 0 ))
|
exit $(( ECHECS_RAPPORT > 0 ? 1 : 0 ))
|
||||||
|
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
# GENERE par Set-OPS (role client_sante).
|
|
||||||
[Unit]
|
|
||||||
Description=Rapporter a Icinga les sondes a la minute (connectivite...)
|
|
||||||
After=network-online.target
|
|
||||||
Wants=network-online.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=oneshot
|
|
||||||
ExecStart=/usr/local/sbin/setops-sante.sh minute
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
# GENERE par Set-OPS (role client_sante).
|
|
||||||
[Unit]
|
|
||||||
Description=Sondes a la minute (connectivite...)
|
|
||||||
|
|
||||||
[Timer]
|
|
||||||
OnBootSec=1min
|
|
||||||
OnUnitActiveSec=60s
|
|
||||||
AccuracySec=5s
|
|
||||||
# Dispersion courte : toute la flotte ne frappe pas l'API a la meme seconde, et le
|
|
||||||
# rapport reste bien en deca du `ttl` (3 min).
|
|
||||||
RandomizedDelaySec=10
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=timers.target
|
|
||||||
|
|
@ -1,96 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
# GENERE par Set-OPS (role client_sante). Ne pas editer a la main.
|
|
||||||
#
|
|
||||||
# SONDE « plancher » — le plancher /etc/hosts de cette machine dit-il la meme chose que
|
|
||||||
# l'autoritatif du domaine ?
|
|
||||||
#
|
|
||||||
# POURQUOI (2026-09-29). Le plancher et la zone PowerDNS derivent du meme plan, mais
|
|
||||||
# chacun ne le suit que quand son role est rejoue. La console a ete ajoutee au plan : la
|
|
||||||
# zone l'a recue, les planchers non — sauf celui d'`ops-01`, redeploye depuis. Depuis
|
|
||||||
# l'edge, `console` ne se resolvait pas, et rien ne le signalait. Sur ces machines, le
|
|
||||||
# plancher est la SEULE resolution des noms internes : un nom qui y manque n'existe pas.
|
|
||||||
#
|
|
||||||
# COMMENT, SANS TRANSFERT DE ZONE. La zone publie `_plancher.<domaine> TXT "n=… sha256=…"`
|
|
||||||
# : le nombre et l'empreinte des paires « nom adresse » que le plancher doit porter pour ce
|
|
||||||
# domaine. On calcule la meme chose ici sur /etc/hosts. Si elles different, on nomme ce
|
|
||||||
# qu'on peut : chaque nom du plancher est demande a l'autoritatif (en trop, readresse), et
|
|
||||||
# l'ecart de nombre dit combien de noms publies manquent ici.
|
|
||||||
#
|
|
||||||
# L'autoritatif est interroge DIRECTEMENT : le resolveur qu'utilisent ces machines peut
|
|
||||||
# ignorer ce domaine (c'est le cas chez les locataires, qui passent par celui du site).
|
|
||||||
#
|
|
||||||
# Mise en defaut PAR PARAMETRE : `client_sante_plancher_fichier` (un plancher vide).
|
|
||||||
set -uo pipefail
|
|
||||||
|
|
||||||
DOMAINE='{{ domaine_interne | default('') }}'
|
|
||||||
AUTORITE='{{ hostvars[groups['serveur_powerdns'][0]].ansible_host | default('') if (groups.get('serveur_powerdns') or []) else '' }}'
|
|
||||||
FICHIER='{{ client_sante_plancher_fichier }}'
|
|
||||||
|
|
||||||
if [[ -z "${DOMAINE}" || -z "${AUTORITE}" ]]; then
|
|
||||||
echo "Pas d'autoritatif interne dans cet ecosysteme : rien a quoi comparer le plancher."
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
txt=$(dig +short +time=3 +tries=2 TXT "_plancher.${DOMAINE}" "@${AUTORITE}" 2>/dev/null | tr -d '"')
|
|
||||||
if [[ "${txt}" != n=*" sha256="* ]]; then
|
|
||||||
echo "L'autoritatif (${AUTORITE}) ne publie pas l'empreinte du plancher (_plancher.${DOMAINE} : ${txt:-rien}) — zone anterieure, ou injoignable : comparaison impossible."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Les paires « nom adresse » du plancher pour CE domaine : toute ligne, tout nom qui finit
|
|
||||||
# par `.<domaine>` (le nom court des hotes, en alias, n'est pas compte ; la zone non plus).
|
|
||||||
local_=$(python3 - "${FICHIER}" "${DOMAINE}" <<'PY'
|
|
||||||
import hashlib, sys
|
|
||||||
fichier, domaine = sys.argv[1], sys.argv[2]
|
|
||||||
paires = set()
|
|
||||||
try:
|
|
||||||
lignes = open(fichier, encoding="utf-8").read().splitlines()
|
|
||||||
except OSError:
|
|
||||||
lignes = []
|
|
||||||
for l in lignes:
|
|
||||||
champs = l.split("#", 1)[0].split()
|
|
||||||
if len(champs) < 2:
|
|
||||||
continue
|
|
||||||
for nom in champs[1:]:
|
|
||||||
if nom.endswith("." + domaine):
|
|
||||||
paires.add(f"{nom} {champs[0]}")
|
|
||||||
tri = sorted(paires)
|
|
||||||
print(f"n={len(tri)} sha256={hashlib.sha256(chr(10).join(tri).encode()).hexdigest()}")
|
|
||||||
for p in tri:
|
|
||||||
print(p)
|
|
||||||
PY
|
|
||||||
)
|
|
||||||
empreinte=$(printf '%s\n' "${local_}" | head -1)
|
|
||||||
n_pub=${txt#n=}; n_pub=${n_pub%% *}
|
|
||||||
n_loc=${empreinte#n=}; n_loc=${n_loc%% *}
|
|
||||||
perf="| noms=${n_loc};;;0; publies=${n_pub};;;0;"
|
|
||||||
|
|
||||||
if [[ "${empreinte}" == "${txt}" ]]; then
|
|
||||||
echo "Plancher conforme a l'autoritatif : ${n_loc} nom(s) de ${DOMAINE}, memes adresses. ${perf}"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ILS DIFFERENT : on nomme ce qu'on peut. Chaque nom du plancher est demande a l'autoritatif.
|
|
||||||
# Un nom qu'il ignore est en trop ici (ou une machine planifiee, que la zone ne publie pas
|
|
||||||
# encore) ; une autre adresse est un readressage que l'un des deux n'a pas suivi.
|
|
||||||
en_trop=(); readresses=(); connus=0; i=0
|
|
||||||
while read -r nom ip; do
|
|
||||||
(( i++ >= 40 )) && break
|
|
||||||
pub=$(dig +short +time=2 +tries=1 A "${nom}" "@${AUTORITE}" 2>/dev/null | tail -1)
|
|
||||||
if [[ -z "${pub}" ]]; then en_trop+=("${nom%%.*}")
|
|
||||||
elif [[ "${pub}" != "${ip}" ]]; then readresses+=("${nom%%.*}=${ip}/${pub}"); connus=$((connus + 1))
|
|
||||||
else connus=$((connus + 1)); fi
|
|
||||||
done < <(printf '%s\n' "${local_}" | tail -n +2)
|
|
||||||
manquants=$(( n_pub - connus ))
|
|
||||||
|
|
||||||
msg="Le plancher de cette machine et l'autoritatif divergent (${n_loc} nom(s) ici, ${n_pub} publie(s))"
|
|
||||||
[[ ${manquants} -gt 0 ]] && msg+=" ; ${manquants} nom(s) publie(s) absent(s) d'ici"
|
|
||||||
[[ -n "${readresses[*]}" ]] && msg+=" ; autre adresse : ${readresses[*]}"
|
|
||||||
[[ -n "${en_trop[*]}" ]] && msg+=" ; inconnu(s) de l'autoritatif : ${en_trop[*]}"
|
|
||||||
msg+=". Rejouer hosts_statiques (ou serveur_powerdns, si c'est la zone qui date)."
|
|
||||||
# Un nom qui MANQUE ou une adresse qui differe casse une resolution : critique. Des noms en
|
|
||||||
# trop seulement (une machine retiree du plan) ne cassent rien : avertissement.
|
|
||||||
if (( manquants > 0 )) || [[ -n "${readresses[*]}" ]]; then
|
|
||||||
echo "${msg} ${perf}"; exit 2
|
|
||||||
fi
|
|
||||||
echo "${msg} ${perf}"; exit 1
|
|
||||||
|
|
@ -6,29 +6,6 @@
|
||||||
#
|
#
|
||||||
# Attendre ici plutot que seulement avant le playbook : le verrou peut etre repris
|
# Attendre ici plutot que seulement avant le playbook : le verrou peut etre repris
|
||||||
# ENTRE deux taches, ce qu'une verification ponctuelle en amont ne peut pas empecher.
|
# ENTRE deux taches, ce qu'une verification ponctuelle en amont ne peut pas empecher.
|
||||||
# APT-GET ATTEND LE VERROU, LUI AUSSI (2026-09-30).
|
|
||||||
#
|
|
||||||
# `lock_timeout` ne protege que la verification faite par le MODULE apt. Pour une mise a
|
|
||||||
# niveau, le module lance ensuite `apt-get dist-upgrade` — qui, lui, n'attend pas : APT 3.0
|
|
||||||
# ne donne d'attente (120 s) qu'a la commande `apt` (`Binary::apt::DPkg::Lock::Timeout`),
|
|
||||||
# pas a `apt-get`. Entre la verification et l'appel, `unattended-upgrades` a repris le
|
|
||||||
# verrou sur `mon-01`, ne une minute plus tot, et la reconstruction de Technolibre s'est
|
|
||||||
# arretee :
|
|
||||||
#
|
|
||||||
# E: Could not get lock /var/lib/dpkg/lock-frontend. It is held by process 1198 (unattended-upgr)
|
|
||||||
#
|
|
||||||
# On le dit donc a APT LUI-MEME, pour tout appel : c'est un fichier, pas une operation apt,
|
|
||||||
# il peut passer en tout premier.
|
|
||||||
- name: Faire attendre le verrou dpkg a tout appel d'APT (apt-get compris)
|
|
||||||
ansible.builtin.copy:
|
|
||||||
dest: /etc/apt/apt.conf.d/10setops-verrou
|
|
||||||
content: |
|
|
||||||
// Gere par Set-OPS (role common_packages). Voir tasks/main.yml.
|
|
||||||
DPkg::Lock::Timeout "{{ common_packages_lock_timeout }}";
|
|
||||||
owner: root
|
|
||||||
group: root
|
|
||||||
mode: "0644"
|
|
||||||
|
|
||||||
# DÉSARMER AVANT TOUTE OPÉRATION APT, ET NON APRÈS.
|
# DÉSARMER AVANT TOUTE OPÉRATION APT, ET NON APRÈS.
|
||||||
#
|
#
|
||||||
# Une tâche apt placée avant celle-ci attendrait le verrou que celle-ci doit justement
|
# Une tâche apt placée avant celle-ci attendrait le verrou que celle-ci doit justement
|
||||||
|
|
@ -110,29 +87,12 @@
|
||||||
when: not (common_packages_desarmer_maj_auto | bool)
|
when: not (common_packages_desarmer_maj_auto | bool)
|
||||||
failed_when: false # une unite absente sur cette image n'est pas une faute
|
failed_when: false # une unite absente sur cette image n'est pas une faute
|
||||||
|
|
||||||
# AVANT TOUTE OPERATION APT : pas de rattrapage herite du gabarit (voir minuteurs.yml).
|
|
||||||
- name: Empecher le rattrapage des minuteurs apt herite du gabarit
|
|
||||||
ansible.builtin.include_tasks: minuteurs.yml
|
|
||||||
when: not (common_packages_desarmer_maj_auto | bool)
|
|
||||||
|
|
||||||
- name: Mettre à jour le cache APT
|
- name: Mettre à jour le cache APT
|
||||||
ansible.builtin.apt:
|
ansible.builtin.apt:
|
||||||
update_cache: true
|
update_cache: true
|
||||||
cache_valid_time: 3600
|
cache_valid_time: 3600
|
||||||
lock_timeout: "{{ common_packages_lock_timeout }}"
|
lock_timeout: "{{ common_packages_lock_timeout }}"
|
||||||
|
|
||||||
# LES ÉPINGLES AVANT LA MISE À JOUR (2026-10-03). `upgrade: full` montait les paquets
|
|
||||||
# tiers au-dela de `paquets-tiers.yml` : au site, `icinga-php-thirdparty` etait passe de
|
|
||||||
# 1.0.0 (pose par le cache) a 1.0.1 sans que personne l'ait decide. Les preferences apt de
|
|
||||||
# TOUTES les epingles sont posees ici, sur toute machine du socle : une epingle sur un
|
|
||||||
# paquet absent ne fait rien, et fixe la version de sa premiere installation.
|
|
||||||
- name: Épingler tous les paquets tiers avant la mise à jour
|
|
||||||
ansible.builtin.include_role:
|
|
||||||
name: paquets_tiers
|
|
||||||
tasks_from: epingles.yml
|
|
||||||
vars:
|
|
||||||
paquets_tiers_toutes_epingles: true
|
|
||||||
|
|
||||||
- name: Appliquer les mises à jour disponibles
|
- name: Appliquer les mises à jour disponibles
|
||||||
ansible.builtin.apt:
|
ansible.builtin.apt:
|
||||||
upgrade: full
|
upgrade: full
|
||||||
|
|
@ -164,16 +124,11 @@
|
||||||
|
|
||||||
- name: Assurer le repertoire d etat des sondes
|
- name: Assurer le repertoire d etat des sondes
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
# 0755, COMME PARTOUT AILLEURS (2026-10-01) : quatre roles tenaient ce repertoire, trois
|
|
||||||
# en 0750 et `serveur_artefacts` en 0755 — apt-cacher-ng doit le traverser pour servir
|
|
||||||
# le depot de binaires du site. Le dernier deploye gagnait : au site, le 2026-10-01, il
|
|
||||||
# rendait 403, et chaque runner allait chercher 500 Mo sur Internet. Le repertoire ne
|
|
||||||
# tient que des etats de sondes, deja lisibles. Garde : test_repertoires_partages.py.
|
|
||||||
path: /var/lib/setops
|
path: /var/lib/setops
|
||||||
state: directory
|
state: directory
|
||||||
owner: root
|
owner: root
|
||||||
group: root
|
group: root
|
||||||
mode: "0755"
|
mode: "0750"
|
||||||
|
|
||||||
- name: Deposer la sonde des correctifs de securite
|
- name: Deposer la sonde des correctifs de securite
|
||||||
ansible.builtin.template:
|
ansible.builtin.template:
|
||||||
|
|
|
||||||
|
|
@ -1,65 +0,0 @@
|
||||||
---
|
|
||||||
# LE RATTRAPAGE HERITE DU GABARIT (2026-10-08).
|
|
||||||
#
|
|
||||||
# `apt-daily.timer` et `apt-daily-upgrade.timer` sont PERSISTANTS : systemd garde la date
|
|
||||||
# de leur dernier passage (`/var/lib/systemd/timers/stamp-*`) et rattrape un passage
|
|
||||||
# manque. Une VM clonee herite de ces traces — celle d'`apt-daily` datait du 2026-06-21,
|
|
||||||
# jour de fabrication du gabarit. Elle « rattrape » donc des son demarrage, avec un delai
|
|
||||||
# aleatoire jusqu'a 60 min : a la reconstruction de Technolibre, `apt-daily-upgrade` est
|
|
||||||
# parti cinq minutes apres la naissance d'`infra-pki-01`, en pleine serie d'operations apt
|
|
||||||
# du role. Il tenait le verrou du CACHE DES ARCHIVES, qu'`apt-get` n'attend jamais
|
|
||||||
# (`DPkg::Lock::Timeout` ne couvre que dpkg) :
|
|
||||||
#
|
|
||||||
# 'apt-get autoremove' failed: E: Could not get lock /var/cache/apt/archives/lock.
|
|
||||||
#
|
|
||||||
# Une trace plus ancienne que la naissance de la machine n'est pas la sienne : on la remet a
|
|
||||||
# l'heure, et on relance le minuteur, qui recalcule son prochain passage (le matin suivant)
|
|
||||||
# sans rattraper. Les mises a jour automatiques restent armees (decision du 2026-09-11) ;
|
|
||||||
# seul disparait un passage herite. Sur une machine en service, la trace est recente :
|
|
||||||
# rien ne change.
|
|
||||||
- name: Naissance de la machine (cle d'hote SSH, posee au premier demarrage)
|
|
||||||
ansible.builtin.stat:
|
|
||||||
path: /etc/ssh/ssh_host_ed25519_key.pub
|
|
||||||
register: common_packages_naissance
|
|
||||||
|
|
||||||
- name: Traces des minuteurs apt persistants
|
|
||||||
ansible.builtin.stat:
|
|
||||||
path: "/var/lib/systemd/timers/stamp-{{ item }}"
|
|
||||||
loop: "{{ common_packages_unites_maj_auto | select('match', '.*[.]timer$') | list }}"
|
|
||||||
register: common_packages_traces
|
|
||||||
|
|
||||||
- name: Remettre a l'heure une trace heritee du gabarit
|
|
||||||
ansible.builtin.file:
|
|
||||||
path: "{{ item.stat.path }}"
|
|
||||||
state: touch
|
|
||||||
mode: "0644"
|
|
||||||
loop: >-
|
|
||||||
{{ common_packages_traces.results | selectattr('stat.exists')
|
|
||||||
| selectattr('stat.mtime', 'lt', common_packages_naissance.stat.mtime | default(0))
|
|
||||||
| list }}
|
|
||||||
loop_control:
|
|
||||||
label: "{{ item.item }}"
|
|
||||||
register: common_packages_traces_remises
|
|
||||||
when: common_packages_naissance.stat.exists
|
|
||||||
|
|
||||||
# Relancer recalcule le prochain passage d'apres la trace neuve. Relancer un minuteur ne
|
|
||||||
# declenche pas sa cible.
|
|
||||||
- name: Relancer les minuteurs dont la trace a ete remise
|
|
||||||
ansible.builtin.systemd_service:
|
|
||||||
name: "{{ item.item.item }}"
|
|
||||||
state: restarted
|
|
||||||
loop: "{{ common_packages_traces_remises.results | default([]) | selectattr('changed') | list }}"
|
|
||||||
loop_control:
|
|
||||||
label: "{{ item.item.item }}"
|
|
||||||
|
|
||||||
# UN PASSAGE DEJA PARTI se termine avant nos operations apt, plutot que de se les disputer.
|
|
||||||
# `is-active` rend 0 si l'une au moins des unites tourne.
|
|
||||||
- name: Attendre la fin d'un passage apt automatique deja en cours
|
|
||||||
ansible.builtin.command:
|
|
||||||
argv: ["systemctl", "is-active", "--quiet", "apt-daily.service", "apt-daily-upgrade.service"]
|
|
||||||
register: common_packages_passage
|
|
||||||
changed_when: false
|
|
||||||
failed_when: false
|
|
||||||
until: common_packages_passage.rc != 0
|
|
||||||
retries: 30
|
|
||||||
delay: 10
|
|
||||||
|
|
@ -19,12 +19,10 @@ ff02::2 ip6-allrouters
|
||||||
{% if hosts_statiques_publier_expositions | default(false) and hosts_statiques_expositions %}
|
{% if hosts_statiques_publier_expositions | default(false) and hosts_statiques_expositions %}
|
||||||
|
|
||||||
# --- Alias d'exposition (FQDN publics → l'edge qui les sert) ---
|
# --- Alias d'exposition (FQDN publics → l'edge qui les sert) ---
|
||||||
{# `resout_vers: service` — entre machines on va au service, l'edge sert aux personnes
|
|
||||||
(voir `expositions_des_applications`). La zone PowerDNS suit la MEME regle. #}
|
|
||||||
{% for expo in hosts_statiques_expositions %}
|
{% for expo in hosts_statiques_expositions %}
|
||||||
{% set cibles = [expo.hote] if expo.resout_vers | default('edge') == 'service' else groups.get(expo.edge, []) %}
|
{% set edge_hosts = groups.get(expo.edge, []) %}
|
||||||
{% if cibles and cibles[0] in hostvars and hostvars[cibles[0]].ansible_host is defined %}
|
{% if edge_hosts and hostvars[edge_hosts[0]].ansible_host is defined %}
|
||||||
{{ "%-15s" | format(hostvars[cibles[0]].ansible_host) }} {{ expo.fqdn }}
|
{{ "%-15s" | format(hostvars[edge_hosts[0]].ansible_host) }} {{ expo.fqdn }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
||||||
|
|
@ -13,16 +13,3 @@ nftables_baseline_extra_udp_ports: []
|
||||||
# source (ip saddr = moindre privilège), dérivées des meta/flux.yml de l'hôte. S'il
|
# source (ip saddr = moindre privilège), dérivées des meta/flux.yml de l'hôte. S'il
|
||||||
# est présent sur le contrôleur, il PRIME sur le gabarit plat ci-dessus.
|
# est présent sur le contrôleur, il PRIME sur le gabarit plat ci-dessus.
|
||||||
nftables_baseline_ruleset_genere: "{{ inventory_dir }}/../../flux-genere/{{ inventory_hostname }}.nft"
|
nftables_baseline_ruleset_genere: "{{ inventory_dir }}/../../flux-genere/{{ inventory_hostname }}.nft"
|
||||||
|
|
||||||
# La liste des flux que la sonde `connectivite` teste chaque minute, generee par `make flux`
|
|
||||||
# a cote du ruleset, depuis les MEMES regles (voir templates/sonde-connectivite.sh.j2).
|
|
||||||
nftables_baseline_connectivite_genere: "{{ inventory_dir }}/../../flux-genere/{{ inventory_hostname }}.connectivite.json"
|
|
||||||
|
|
||||||
# LA PASSERELLE N'EST PAS UNE PORTE (2026-10-05). Ports qui ne doivent JAMAIS repondre sur la
|
|
||||||
# passerelle par defaut de la machine — la sonde `connectivite` le verifie chaque minute.
|
|
||||||
# Chez un locataire en SDN, la passerelle de zone est l'HYPERVISEUR lui-meme, dans le VRF du
|
|
||||||
# locataire : une API Proxmox qui y repondrait contournerait la frontiere. Fermee aujourd'hui
|
|
||||||
# (mesure du 2026-10-05 : ECONNREFUSED) ; la garde dit si elle s'ouvre, quelle qu'en soit la
|
|
||||||
# cause (`net.ipv4.tcp_l3mdev_accept`, un pare-feu d'hote retire...). Sur une machine du site,
|
|
||||||
# la passerelle est la frontiere, qui n'ecoute pas sur ces ports.
|
|
||||||
nftables_baseline_connectivite_passerelle_interdits: [8006]
|
|
||||||
|
|
|
||||||
|
|
@ -80,49 +80,3 @@
|
||||||
state: stopped
|
state: stopped
|
||||||
when: not (nftables_baseline_enabled | bool)
|
when: not (nftables_baseline_enabled | bool)
|
||||||
failed_when: false
|
failed_when: false
|
||||||
|
|
||||||
# --- LA SONDE « connectivite » (2026-09-30) ------------------------------------------------
|
|
||||||
#
|
|
||||||
# Ce que ce pare-feu promet, verifie chaque minute depuis la machine qui doit pouvoir
|
|
||||||
# joindre — et non plus seulement au moment d'une activation. Deposee dans le repertoire
|
|
||||||
# des sondes A LA MINUTE, que le porteur `client_sante` joue en mode `minute`.
|
|
||||||
- name: La liste des flux de cet hote est-elle generee ?
|
|
||||||
ansible.builtin.stat:
|
|
||||||
path: "{{ nftables_baseline_connectivite_genere }}"
|
|
||||||
delegate_to: localhost
|
|
||||||
become: false
|
|
||||||
changed_when: false
|
|
||||||
register: nftables_baseline_connectivite_stat
|
|
||||||
|
|
||||||
- name: Assurer le repertoire des sondes a la minute
|
|
||||||
ansible.builtin.file:
|
|
||||||
path: /usr/local/lib/setops/sondes-minute
|
|
||||||
state: directory
|
|
||||||
owner: root
|
|
||||||
group: root
|
|
||||||
mode: "0755"
|
|
||||||
|
|
||||||
# PAS DANS /etc/setops : cette liste n'a rien de secret, et ce repertoire-la — celui des
|
|
||||||
# secrets — a deja ses gardiens (0700 chez les locataires, 0755 au site). Un troisieme avis
|
|
||||||
# sur ses droits le faisait basculer a chaque passage (vu en simulation au site).
|
|
||||||
- name: Deposer la liste des flux a tester
|
|
||||||
ansible.builtin.copy:
|
|
||||||
src: "{{ nftables_baseline_connectivite_genere }}"
|
|
||||||
dest: /usr/local/lib/setops/connectivite.json
|
|
||||||
owner: root
|
|
||||||
group: root
|
|
||||||
mode: "0644"
|
|
||||||
when: nftables_baseline_connectivite_stat.stat.exists
|
|
||||||
|
|
||||||
- name: Retirer l'ancien emplacement de la liste (premier deploiement, 2026-09-30)
|
|
||||||
ansible.builtin.file:
|
|
||||||
path: /etc/setops/connectivite.json
|
|
||||||
state: absent
|
|
||||||
|
|
||||||
- name: Deposer la sonde « connectivite »
|
|
||||||
ansible.builtin.template:
|
|
||||||
src: sonde-connectivite.sh.j2
|
|
||||||
dest: /usr/local/lib/setops/sondes-minute/connectivite.sh
|
|
||||||
owner: root
|
|
||||||
group: root
|
|
||||||
mode: "0750"
|
|
||||||
|
|
|
||||||
|
|
@ -7,25 +7,25 @@ table inet setops_filter {
|
||||||
chain input {
|
chain input {
|
||||||
type filter hook input priority 0; policy {{ nftables_baseline_input_policy }};
|
type filter hook input priority 0; policy {{ nftables_baseline_input_policy }};
|
||||||
|
|
||||||
iif "lo" accept comment "socle: communications locales"
|
iif "lo" accept
|
||||||
ct state established,related accept comment "socle: connexions etablies et associees"
|
ct state established,related accept
|
||||||
ct state invalid drop comment "socle: paquets invalides"
|
ct state invalid drop
|
||||||
|
|
||||||
{% if nftables_baseline_allow_icmp | bool %}
|
{% if nftables_baseline_allow_icmp | bool %}
|
||||||
ip protocol icmp accept comment "socle: diagnostic et erreurs reseau IPv4"
|
ip protocol icmp accept
|
||||||
ip6 nexthdr icmpv6 accept comment "socle: diagnostic et erreurs reseau IPv6"
|
ip6 nexthdr icmpv6 accept
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if nftables_baseline_allow_ssh | bool %}
|
{% if nftables_baseline_allow_ssh | bool %}
|
||||||
tcp dport {{ nftables_baseline_ssh_port }} accept comment "administration: acces SSH du gabarit de repli"
|
tcp dport {{ nftables_baseline_ssh_port }} accept
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% for port in nftables_baseline_extra_tcp_ports %}
|
{% for port in nftables_baseline_extra_tcp_ports %}
|
||||||
tcp dport {{ port }} accept comment "repli: port TCP supplementaire declare par l'instance"
|
tcp dport {{ port }} accept
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
{% for port in nftables_baseline_extra_udp_ports %}
|
{% for port in nftables_baseline_extra_udp_ports %}
|
||||||
udp dport {{ port }} accept comment "repli: port UDP supplementaire declare par l'instance"
|
udp dport {{ port }} accept
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,189 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
# GENERE par Set-OPS (role nftables_baseline). Ne pas editer a la main.
|
|
||||||
#
|
|
||||||
# SONDE « connectivite » — chaque flux SORTANT que le registre des flux promet a cette
|
|
||||||
# machine passe-t-il, en ce moment ? Rapportee chaque minute (porteur `client_sante`, mode
|
|
||||||
# `minute`) : un flux coupe se voit dans la minute, pas au prochain deploiement.
|
|
||||||
#
|
|
||||||
# POURQUOI (2026-09-30). L'activation du pare-feu Proxmox se prouvait par une matrice rejouee
|
|
||||||
# VM par VM, avant et apres : vingt minutes en fin de reconstruction, et une preuve valable
|
|
||||||
# a l'instant de l'activation seulement. Cette sonde la remplace, et veille ensuite en
|
|
||||||
# permanence.
|
|
||||||
#
|
|
||||||
# LA LISTE n'est pas ecrite ici : `make flux` la derive des MEMES regles resolues que le
|
|
||||||
# `.nft` de chaque hote (`flux-genere/<hote>.connectivite.json`). On teste ce que le pare-feu
|
|
||||||
# applique, pas ce qu'on croit.
|
|
||||||
#
|
|
||||||
# CE QUE L'ERRNO DIT — mesure du 2026-09-30, depuis web-frontal-01 de Technolibre :
|
|
||||||
# ouvert le flux passe -> OK
|
|
||||||
# ECONNREFUSED REFUSE : personne n'ecoute, OU la frontiere refuse -> AVERTISSEMENT
|
|
||||||
# Depuis le 2026-10-01 les interfaces internes de la frontiere rendent un
|
|
||||||
# `reject` — un RST pour TCP, indiscernable d'un port ferme. Et depuis les
|
|
||||||
# `seulement_si`, aucun flux declare ne mene plus a un port sans service :
|
|
||||||
# ce cas n'est plus jamais normal. Jaune, pas rouge — un service qui
|
|
||||||
# redemarre ne doit pas reveiller quelqu'un.
|
|
||||||
# EHOSTUNREACH REJETE (pare-feu Proxmox : icmp host-prohibited), ou
|
|
||||||
# machine injoignable -> CRITIQUE
|
|
||||||
# delai JETE en silence (politique `drop` de nftables) -> CRITIQUE
|
|
||||||
# Un `connect()` TCP ne prouve pas une livraison au-dela d'un relais ; ici les flux sont
|
|
||||||
# directs (meme locataire, routes par la frontiere sans relais applicatif), et c'est bien
|
|
||||||
# le passage des pare-feux qu'on mesure.
|
|
||||||
#
|
|
||||||
# CE QU'ELLE OBSERVE AUSSI : les connexions entrantes ETABLIES que les entrees declarees ne
|
|
||||||
# couvrent pas (AVERTISSEMENT). Un flux reel non declare serait coupe a la prochaine regle
|
|
||||||
# posee : mieux vaut l'apprendre avant.
|
|
||||||
#
|
|
||||||
# CE QU'ELLE GARDE AUSSI (2026-10-05) : la passerelle par defaut ne doit repondre sur aucun
|
|
||||||
# port de `nftables_baseline_connectivite_passerelle_interdits` (l'API Proxmox, 8006). Chez un
|
|
||||||
# locataire en SDN, cette passerelle est l'hyperviseur, dans le VRF du locataire ; une API qui y
|
|
||||||
# repondrait contournerait la frontiere. Ici, `ouvert` prouve bien quelque chose : entre la
|
|
||||||
# machine et sa passerelle, aucun relais. Ouvert -> CRITIQUE ; refuse, rejete, delai -> attendu.
|
|
||||||
# Passerelle introuvable -> AVERTISSEMENT : une garde qui ne s'exerce pas doit le dire.
|
|
||||||
#
|
|
||||||
# Contrat : une ligne, code 0/1/2 (docs/supervision-conception.md).
|
|
||||||
# `CONNECTIVITE_JSON=<fichier>` impose une autre liste : c'est le controle negatif.
|
|
||||||
# `CONNECTIVITE_PASSERELLE=<adresse>` vise une autre « passerelle » : le controle negatif de la
|
|
||||||
# garde (une adresse dont un port interdit repond doit la faire crier).
|
|
||||||
exec python3 - "${CONNECTIVITE_JSON:-/usr/local/lib/setops/connectivite.json}" <<'PY'
|
|
||||||
import concurrent.futures, errno, ipaddress, json, os, socket, subprocess, sys
|
|
||||||
|
|
||||||
INTERDITS = {{ nftables_baseline_connectivite_passerelle_interdits | to_json }}
|
|
||||||
|
|
||||||
try:
|
|
||||||
d = json.load(open(sys.argv[1], encoding="utf-8"))
|
|
||||||
except (OSError, ValueError) as e:
|
|
||||||
print(f"Liste des flux absente ou illisible ({sys.argv[1]}) : rien n'est teste. "
|
|
||||||
"Regenerer `make flux` et redeployer nftables_baseline.")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
def tcp(ip, port):
|
|
||||||
s = socket.socket()
|
|
||||||
s.settimeout(2)
|
|
||||||
try:
|
|
||||||
s.connect((ip, int(port)))
|
|
||||||
return "ouvert"
|
|
||||||
except socket.timeout:
|
|
||||||
return "delai"
|
|
||||||
except OSError as e:
|
|
||||||
if e.errno == errno.ECONNREFUSED:
|
|
||||||
return "refuse"
|
|
||||||
if e.errno in (errno.EHOSTUNREACH, errno.ENETUNREACH, errno.EACCES, errno.EPERM):
|
|
||||||
return "rejete"
|
|
||||||
return errno.errorcode.get(e.errno, str(e.errno))
|
|
||||||
finally:
|
|
||||||
s.close()
|
|
||||||
|
|
||||||
|
|
||||||
def ping(ip):
|
|
||||||
r = subprocess.run(["ping", "-c1", "-W2", ip], capture_output=True)
|
|
||||||
return "ouvert" if r.returncode == 0 else "delai"
|
|
||||||
|
|
||||||
|
|
||||||
def essai(f):
|
|
||||||
return f, (ping(f["ip"]) if f["proto"] == "icmp" else tcp(f["ip"], f["port"]))
|
|
||||||
|
|
||||||
|
|
||||||
sorties = d.get("sorties") or []
|
|
||||||
with concurrent.futures.ThreadPoolExecutor(max_workers=16) as ex:
|
|
||||||
resultats = list(ex.map(essai, sorties))
|
|
||||||
|
|
||||||
coupes = [(f, r) for f, r in resultats if r not in ("ouvert", "refuse")]
|
|
||||||
|
|
||||||
# La passerelle n'est pas une porte.
|
|
||||||
def passerelle_par_defaut():
|
|
||||||
if os.environ.get("CONNECTIVITE_PASSERELLE"):
|
|
||||||
return os.environ["CONNECTIVITE_PASSERELLE"]
|
|
||||||
try:
|
|
||||||
for l in subprocess.run(["ip", "-4", "route", "show", "default"], capture_output=True,
|
|
||||||
text=True, timeout=5).stdout.splitlines():
|
|
||||||
c = l.split()
|
|
||||||
if "via" in c:
|
|
||||||
return c[c.index("via") + 1]
|
|
||||||
except (OSError, subprocess.TimeoutExpired):
|
|
||||||
pass
|
|
||||||
return None
|
|
||||||
|
|
||||||
gw = passerelle_par_defaut()
|
|
||||||
porte = [p for p in INTERDITS if gw and tcp(gw, p) == "ouvert"]
|
|
||||||
personne = [(f, r) for f, r in resultats if r == "refuse"]
|
|
||||||
ouverts = len(resultats) - len(coupes) - len(personne)
|
|
||||||
|
|
||||||
# Entrees etablies hors des regles declarees.
|
|
||||||
def lignes(cmd):
|
|
||||||
try:
|
|
||||||
return subprocess.run(cmd, capture_output=True, text=True, timeout=10).stdout.splitlines()
|
|
||||||
except (OSError, subprocess.TimeoutExpired):
|
|
||||||
return []
|
|
||||||
|
|
||||||
ecoute = {l.split()[3].rsplit(":", 1)[1] for l in lignes(["ss", "-Hltn"]) if len(l.split()) >= 4}
|
|
||||||
# MES PROPRES ADRESSES : une machine qui se parle a elle-meme par son adresse (Loki sur
|
|
||||||
# obs-01, port 9096) passe par la boucle locale, jamais par un pare-feu. Vu au premier
|
|
||||||
# deploiement : `obs-01` se declarait « hors des regles » envers lui-meme.
|
|
||||||
moi = set()
|
|
||||||
for l in lignes(["ip", "-o", "addr", "show"]):
|
|
||||||
c = l.split()
|
|
||||||
if len(c) >= 4 and c[2] in ("inet", "inet6"):
|
|
||||||
moi.add(c[3].split("/")[0])
|
|
||||||
reseaux = {}
|
|
||||||
for e in d.get("entrees") or []:
|
|
||||||
if e.get("proto") == "tcp":
|
|
||||||
reseaux.setdefault(str(e["port"]), []).extend(e.get("sources") or [])
|
|
||||||
reseaux.setdefault("22", []).extend(d.get("admin_ssh") or [])
|
|
||||||
hors = set()
|
|
||||||
for l in lignes(["ss", "-Hnt", "state", "established"]):
|
|
||||||
c = l.split()
|
|
||||||
if len(c) < 4:
|
|
||||||
continue
|
|
||||||
lport = c[2].rsplit(":", 1)[1]
|
|
||||||
pair = c[3].rsplit(":", 1)[0].strip("[]").replace("::ffff:", "")
|
|
||||||
if lport not in ecoute:
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
adr = ipaddress.ip_address(pair)
|
|
||||||
except ValueError:
|
|
||||||
continue
|
|
||||||
if adr.is_loopback or str(adr) in moi:
|
|
||||||
continue
|
|
||||||
permis = False
|
|
||||||
for r in reseaux.get(lport, []):
|
|
||||||
try:
|
|
||||||
if adr in ipaddress.ip_network(str(r), strict=False):
|
|
||||||
permis = True
|
|
||||||
break
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
if not permis:
|
|
||||||
hors.add(f"{pair}->{lport}")
|
|
||||||
|
|
||||||
def nomme(xs):
|
|
||||||
return ", ".join(f"{f['vers']}:{f['port'] if f['proto'] != 'icmp' else 'ping'} ({r})" for f, r in xs[:5]) \
|
|
||||||
+ (f" et {len(xs) - 5} autre(s)" if len(xs) > 5 else "")
|
|
||||||
|
|
||||||
perf = (f"| ouverts={ouverts};;;0 coupes={len(coupes)};;;0 sans_service={len(personne)};;;0 "
|
|
||||||
f"hors_regles={len(hors)};;;0 passerelle_ouverte={len(porte)};;;0")
|
|
||||||
if porte:
|
|
||||||
print(f"PASSERELLE OUVERTE : {gw} repond sur {', '.join(map(str, porte))} — la passerelle de zone "
|
|
||||||
f"(l'hyperviseur, chez un locataire) expose ce qui doit rester derriere la frontiere. {perf}")
|
|
||||||
sys.exit(2)
|
|
||||||
if coupes:
|
|
||||||
print(f"COUPE : {len(coupes)}/{len(resultats)} flux sortant(s) ne passent pas — {nomme(coupes)}. {perf}")
|
|
||||||
sys.exit(2)
|
|
||||||
avert = []
|
|
||||||
info = ""
|
|
||||||
if INTERDITS and not gw:
|
|
||||||
avert.append("passerelle par defaut introuvable — la garde de la passerelle ne s'exerce pas")
|
|
||||||
if personne:
|
|
||||||
avert.append(f"{len(personne)} flux refuse(s) — service absent, ou refus de la frontiere — "
|
|
||||||
f"{nomme(personne)}")
|
|
||||||
if hors:
|
|
||||||
avert.append(f"{len(hors)} connexion(s) entrante(s) hors des regles declarees — "
|
|
||||||
+ ", ".join(sorted(hors)[:5]))
|
|
||||||
if avert:
|
|
||||||
print(f"{ouverts}/{len(resultats)} flux sortants ouverts{info} ; " + " ; ".join(avert) + f". {perf}")
|
|
||||||
sys.exit(1)
|
|
||||||
if not resultats:
|
|
||||||
print(f"Aucun flux sortant declare pour {d.get('hote', 'cet hote')} ; aucune entree hors des regles. {perf}")
|
|
||||||
sys.exit(0)
|
|
||||||
print(f"Connectivite : {ouverts}/{len(resultats)} flux sortants ouverts{info} ; aucune entree hors des regles. {perf}")
|
|
||||||
PY
|
|
||||||
|
|
@ -20,19 +20,3 @@ paquets_tiers_noms: []
|
||||||
# télécharge une fois depuis un poste qui a Internet, on déploie ensuite sans en avoir.
|
# télécharge une fois depuis un poste qui a Internet, on déploie ensuite sans en avoir.
|
||||||
paquets_tiers_cache: "{{ lookup('env', 'HOME') }}/.cache/setops/paquets"
|
paquets_tiers_cache: "{{ lookup('env', 'HOME') }}/.cache/setops/paquets"
|
||||||
paquets_tiers_dest: "/var/cache/setops/paquets"
|
paquets_tiers_dest: "/var/cache/setops/paquets"
|
||||||
|
|
||||||
# LES ÉPINGLES S'APPLIQUENT AUSSI SANS CACHE (2026-10-03). Elles ne valaient que pour ce
|
|
||||||
# qui sortait du cache du contrôleur. Un runner — qui n'a pas ce cache — installait la
|
|
||||||
# dernière version publiée, et le `dist-upgrade` du socle (`common_packages`) montait tout
|
|
||||||
# au-delà de l'épingle, cache ou pas. Mesuré le jour même : sept paquets sur vingt et un
|
|
||||||
# plus récents que leur épingle chez les locataires, et un au site.
|
|
||||||
#
|
|
||||||
# Le fichier qui fait foi est celui du dépôt — le même que lit `make cacher-paquets`.
|
|
||||||
paquets_tiers_fichier_epingles: "{{ role_path }}/../../paquets-tiers.yml"
|
|
||||||
# 990 : apt PRÉFÈRE la version épinglée à toute version plus récente (500 par défaut), y
|
|
||||||
# monte un paquet plus ancien, mais ne RÉTROGRADE jamais un paquet déjà plus récent — ce
|
|
||||||
# que ferait 1000 et plus. Éprouvé sur `site-dns-01` et `web-dorsal-01` avant d'écrire.
|
|
||||||
paquets_tiers_priorite: 990
|
|
||||||
# `true` : épingler TOUS les paquets de paquets-tiers.yml, pas seulement `paquets_tiers_noms`.
|
|
||||||
# C'est l'appel du socle, avant son `dist-upgrade` (voir common_packages).
|
|
||||||
paquets_tiers_toutes_epingles: false
|
|
||||||
|
|
|
||||||
|
|
@ -1,75 +0,0 @@
|
||||||
---
|
|
||||||
# LES ÉPINGLES, POSÉES COMME PRÉFÉRENCES APT — cache ou pas (voir defaults/main.yml).
|
|
||||||
# Une préférence par paquet : plusieurs rôles appellent celui-ci sur la même machine, et
|
|
||||||
# un fichier commun serait réécrit par chacun avec ses seuls paquets.
|
|
||||||
- name: Lire les épingles du dépôt
|
|
||||||
ansible.builtin.set_fact:
|
|
||||||
paquets_tiers_toutes: >-
|
|
||||||
{{ (lookup('ansible.builtin.file', paquets_tiers_fichier_epingles) | from_yaml).depots
|
|
||||||
| dict2items | map(attribute='value.paquets') | list | combine }}
|
|
||||||
|
|
||||||
- name: Retenir celles de cet appel (toutes, pour le socle)
|
|
||||||
ansible.builtin.set_fact:
|
|
||||||
paquets_tiers_epingles: >-
|
|
||||||
{{ paquets_tiers_toutes if (paquets_tiers_toutes_epingles | bool)
|
|
||||||
else (paquets_tiers_toutes | dict2items | selectattr('key', 'in', paquets_tiers_noms)
|
|
||||||
| items2dict) }}
|
|
||||||
|
|
||||||
- name: Épingler les paquets tiers (préférence apt)
|
|
||||||
ansible.builtin.copy:
|
|
||||||
dest: "/etc/apt/preferences.d/setops-{{ item.key }}"
|
|
||||||
content: |
|
|
||||||
# Géré par Set-OPS (rôle paquets_tiers, depuis paquets-tiers.yml). Ne pas éditer.
|
|
||||||
Package: {{ item.key }}
|
|
||||||
Pin: version {{ item.value }}
|
|
||||||
Pin-Priority: {{ paquets_tiers_priorite }}
|
|
||||||
owner: root
|
|
||||||
group: root
|
|
||||||
mode: "0644"
|
|
||||||
loop: "{{ paquets_tiers_epingles | dict2items }}"
|
|
||||||
loop_control:
|
|
||||||
label: "{{ item.key }}={{ item.value }}"
|
|
||||||
|
|
||||||
# CE QUE L'ÉPINGLE NE PEUT PAS FAIRE, ET QU'IL FAUT DIRE. Deux cas où elle ne tient pas :
|
|
||||||
# - le paquet est DÉJÀ plus récent : 990 ne rétrograde pas (c'est voulu) — l'épingle est
|
|
||||||
# en retard sur la machine, et il faut la relever, pas laisser l'écart dormir ;
|
|
||||||
# - la version épinglée n'est PAS dans l'index du dépôt : apt ignore la préférence sans
|
|
||||||
# un mot et reprend la dernière version — la candidate est alors plus récente.
|
|
||||||
# Ce second contrôle n'a pas cours en simulation : la préférence n'y est pas écrite, et la
|
|
||||||
# candidate lue serait celle d'avant l'épingle — une fausse alerte à chaque `--check`.
|
|
||||||
- name: Comparer installé, candidat et épingle
|
|
||||||
ansible.builtin.shell: |
|
|
||||||
set -uo pipefail
|
|
||||||
{% for nom, version in paquets_tiers_epingles.items() %}
|
|
||||||
inst=$(dpkg-query -W -f='${Version}' {{ nom }} 2>/dev/null || true)
|
|
||||||
cand=$(apt-cache policy {{ nom }} 2>/dev/null | awk '/Candidate:/ {print $2}')
|
|
||||||
if [ -n "$inst" ] && dpkg --compare-versions "$inst" gt "{{ version }}"; then
|
|
||||||
echo "PLUS_RECENT {{ nom }} installe=$inst epingle={{ version }}"
|
|
||||||
fi
|
|
||||||
{% if not ansible_check_mode %}
|
|
||||||
if [ -n "$cand" ] && [ "$cand" != "(none)" ] && dpkg --compare-versions "$cand" gt "{{ version }}" \
|
|
||||||
&& { [ -z "$inst" ] || dpkg --compare-versions "$cand" gt "$inst"; }; then
|
|
||||||
echo "INOPERANTE {{ nom }} candidat=$cand epingle={{ version }}"
|
|
||||||
fi
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
true
|
|
||||||
args:
|
|
||||||
executable: /bin/bash
|
|
||||||
environment:
|
|
||||||
LC_ALL: C
|
|
||||||
register: paquets_tiers_ecarts
|
|
||||||
changed_when: false
|
|
||||||
check_mode: false
|
|
||||||
when: paquets_tiers_epingles | length > 0
|
|
||||||
|
|
||||||
- name: Refuser une épingle qui ne tient pas
|
|
||||||
ansible.builtin.assert:
|
|
||||||
that:
|
|
||||||
- paquets_tiers_ecarts.stdout_lines | default([]) | length == 0
|
|
||||||
fail_msg: >-
|
|
||||||
Épingle(s) de paquets-tiers.yml qui ne tiennent pas sur {{ inventory_hostname }} :
|
|
||||||
{{ paquets_tiers_ecarts.stdout_lines | default([]) | join(' ; ') }}.
|
|
||||||
PLUS_RECENT : relever l'épingle sur la version installée (puis `make cacher-paquets`).
|
|
||||||
INOPERANTE : la version épinglée n'est pas dans l'index du dépôt — en choisir une qui y est.
|
|
||||||
when: paquets_tiers_epingles | length > 0
|
|
||||||
|
|
@ -2,10 +2,6 @@
|
||||||
# DÉGRADER, JAMAIS DEVINER. Sans cache sur le contrôleur, ce rôle ne fait rien et le DIT :
|
# DÉGRADER, JAMAIS DEVINER. Sans cache sur le contrôleur, ce rôle ne fait rien et le DIT :
|
||||||
# l'appelant installera depuis le dépôt distant, comme avant. On ne casse pas un
|
# l'appelant installera depuis le dépôt distant, comme avant. On ne casse pas un
|
||||||
# déploiement connecté parce qu'un cache manque.
|
# déploiement connecté parce qu'un cache manque.
|
||||||
# LES ÉPINGLES D'ABORD — voir `epingles.yml`, que le socle appelle aussi pour TOUTES.
|
|
||||||
- name: Épingler les paquets demandés (préférences apt)
|
|
||||||
ansible.builtin.include_tasks: epingles.yml
|
|
||||||
|
|
||||||
- name: Lire le manifeste du cache du contrôleur
|
- name: Lire le manifeste du cache du contrôleur
|
||||||
ansible.builtin.slurp:
|
ansible.builtin.slurp:
|
||||||
src: "{{ paquets_tiers_cache }}/MANIFESTE"
|
src: "{{ paquets_tiers_cache }}/MANIFESTE"
|
||||||
|
|
@ -69,13 +65,6 @@
|
||||||
loop_control:
|
loop_control:
|
||||||
label: "{{ item.key }}"
|
label: "{{ item.key }}"
|
||||||
|
|
||||||
# GARDER LES FICHIERS DE CONFIGURATION QUE LES RÔLES ONT MODIFIÉS (2026-10-03). Tant que ce
|
|
||||||
# rôle ne faisait que des PREMIÈRES installations, aucun fichier n'avait été touché. Depuis
|
|
||||||
# que les épingles montent, il MET À JOUR : sur `site-dns-01`, `alloy` 1.19.2 -> 1.20.1 a
|
|
||||||
# demandé quoi faire de `/etc/default/alloy` (le port y est imposé par `client_journal`),
|
|
||||||
# sans personne pour répondre — « fin de fichier sur l'entrée standard », paquet déballé et
|
|
||||||
# NON configuré. Mêmes options que le socle (`common_packages`) : garder la version locale.
|
|
||||||
#
|
|
||||||
# EN UN SEUL APPEL, ET C'EST LE POINT. `apt` sait résoudre un ENSEMBLE de fichiers locaux
|
# EN UN SEUL APPEL, ET C'EST LE POINT. `apt` sait résoudre un ENSEMBLE de fichiers locaux
|
||||||
# — l'un dépendant de l'autre — alors qu'une installation paquet par paquet échouerait sur
|
# — l'un dépendant de l'autre — alors qu'une installation paquet par paquet échouerait sur
|
||||||
# l'ordre. Icinga en apporte dix-sept qui se dépendent mutuellement : les poser un à un
|
# l'ordre. Icinga en apporte dix-sept qui se dépendent mutuellement : les poser un à un
|
||||||
|
|
@ -85,16 +74,12 @@
|
||||||
- name: Installer les paquets tiers depuis le cache
|
- name: Installer les paquets tiers depuis le cache
|
||||||
ansible.builtin.command:
|
ansible.builtin.command:
|
||||||
argv: >-
|
argv: >-
|
||||||
{{ ["apt-get", "install", "-y", "--no-install-recommends",
|
{{ ["apt-get", "install", "-y", "--no-install-recommends"]
|
||||||
"-o", "Dpkg::Options::=--force-confdef", "-o", "Dpkg::Options::=--force-confold"]
|
|
||||||
+ (paquets_tiers_disponibles.values()
|
+ (paquets_tiers_disponibles.values()
|
||||||
| map('regex_replace', '^', paquets_tiers_dest ~ '/') | list) }}
|
| map('regex_replace', '^', paquets_tiers_dest ~ '/') | list) }}
|
||||||
environment:
|
environment:
|
||||||
DEBIAN_FRONTEND: noninteractive
|
DEBIAN_FRONTEND: noninteractive
|
||||||
register: paquets_tiers_pose
|
register: paquets_tiers_pose
|
||||||
# UNE MISE À JOUR EST UN CHANGEMENT (2026-10-03). La garde ne cherchait que « 0 nouvellement
|
changed_when: "'0 nouvellement installés' not in paquets_tiers_pose.stdout
|
||||||
# installés », qu'apt écrit AUSSI quand il met à jour (« 1 mis à jour, 0 nouvellement
|
and '0 newly installed' not in paquets_tiers_pose.stdout"
|
||||||
# installés ») : `alloy` 1.19.2 -> 1.20.1 sur `site-cache-01` s'est rapporté « ok ».
|
|
||||||
changed_when: "'0 mis à jour, 0 nouvellement installés' not in paquets_tiers_pose.stdout
|
|
||||||
and '0 upgraded, 0 newly installed' not in paquets_tiers_pose.stdout"
|
|
||||||
when: paquets_tiers_disponibles | length > 0
|
when: paquets_tiers_disponibles | length > 0
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ d'annuaire.
|
||||||
## Variables
|
## Variables
|
||||||
| Variable | Défaut | Rôle |
|
| Variable | Défaut | Rôle |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `resoudre_annuaire_hote` | `id-ldap-01` | **Le** point où le nom d'hôte est fixé (surchargeable par instance) |
|
| `resoudre_annuaire_hote` | `""` | Dérivé du PLAN (`applications.<app>.hote`) ; surchargeable par instance. Vide et non dérivé, le rôle REFUSE plutôt que de deviner |
|
||||||
| `resoudre_annuaire_port` | `636` | Port LDAPS |
|
| `resoudre_annuaire_port` | `636` | Port LDAPS |
|
||||||
| `resoudre_annuaire_encryption` | `ldaps` | Schéma de l'URI |
|
| `resoudre_annuaire_encryption` | `ldaps` | Schéma de l'URI |
|
||||||
| `resoudre_annuaire_bind_cn` | `admin` | CN du compte de bind |
|
| `resoudre_annuaire_bind_cn` | `admin` | CN du compte de bind |
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,19 @@
|
||||||
# aucune machine du plan (l'annuaire vit sur `idm-01`), et Postfix ne pouvait pas se lier.
|
# aucune machine du plan (l'annuaire vit sur `idm-01`), et Postfix ne pouvait pas se lier.
|
||||||
# Constaté le 2026-08-07. Le repli ci-dessous ne sert que si le plan est muet.
|
# Constaté le 2026-08-07. Le repli ci-dessous ne sert que si le plan est muet.
|
||||||
resoudre_annuaire_app: "openldap"
|
resoudre_annuaire_app: "openldap"
|
||||||
resoudre_annuaire_hote: "id-ldap-01"
|
# VIDE, ET C'EST LE DEFAUT LUI-MEME QUI ETAIT LE PIEGE. Un nom ecrit ici tient des
|
||||||
|
# que le plan se tait : la derivation est sautee, le role bâtit
|
||||||
|
# `ldaps://<ce nom>.<domaine>`, et le consommateur echoue a l'execution sur
|
||||||
|
# « Unable to bind » en nommant une machine qui n'est dans aucun plan.
|
||||||
|
#
|
||||||
|
# La correction qui a introduit la derivation depuis le plan a laisse ce defaut en
|
||||||
|
# place ; il ramene donc l'ancien comportement chaque fois que l'application n'est
|
||||||
|
# pas au plan. Vide, l'assertion qui suit refuse AU DEPLOIEMENT, la ou le nom
|
||||||
|
# devine ne se voyait qu'au premier bind.
|
||||||
|
#
|
||||||
|
# Une instance garde le droit de le poser explicitement : ce qui disparait est la
|
||||||
|
# valeur DEVINEE, pas la surcharge.
|
||||||
|
resoudre_annuaire_hote: ""
|
||||||
resoudre_annuaire_port: 636
|
resoudre_annuaire_port: 636
|
||||||
resoudre_annuaire_encryption: "ldaps"
|
resoudre_annuaire_encryption: "ldaps"
|
||||||
# COMPTE DE LIAISON — CHAQUE CONSOMMATEUR A LE SIEN (2026-09-13).
|
# COMPTE DE LIAISON — CHAQUE CONSOMMATEUR A LE SIEN (2026-09-13).
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,17 @@
|
||||||
resoudre_annuaire_hote: "{{ (applications | default({})).get(resoudre_annuaire_app, {}).get('hote') }}"
|
resoudre_annuaire_hote: "{{ (applications | default({})).get(resoudre_annuaire_app, {}).get('hote') }}"
|
||||||
when: ((applications | default({})).get(resoudre_annuaire_app, {}).get('hote') | default('', true)) | length > 0
|
when: ((applications | default({})).get(resoudre_annuaire_app, {}).get('hote') | default('', true)) | length > 0
|
||||||
|
|
||||||
|
- name: Exiger que le plan nomme l'hôte de l'annuaire
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- resoudre_annuaire_hote | length > 0
|
||||||
|
fail_msg: >-
|
||||||
|
L'application « {{ resoudre_annuaire_app }} » n'est pas au plan, et aucune
|
||||||
|
instance ne pose `resoudre_annuaire_hote` : rien ne nomme l'annuaire. Le rôle
|
||||||
|
qui le demande ne peut pas s'y lier. Un nom deviné ici produirait
|
||||||
|
« Unable to bind » sur une machine qui n'est dans aucun plan — et le
|
||||||
|
déploiement passerait au VERT jusque-là.
|
||||||
|
|
||||||
# set_fact (pas les defaults) pour que ces valeurs persistent dans le rôle consommateur
|
# set_fact (pas les defaults) pour que ces valeurs persistent dans le rôle consommateur
|
||||||
# après include_role (les defaults du rôle inclus ne fuient pas hors de son exécution).
|
# après include_role (les defaults du rôle inclus ne fuient pas hors de son exécution).
|
||||||
- name: Dériver la connexion à l'annuaire
|
- name: Dériver la connexion à l'annuaire
|
||||||
|
|
|
||||||
|
|
@ -19,13 +19,9 @@ quarante minutes sur la **première** machine.
|
||||||
| | |
|
| | |
|
||||||
|---|---|
|
|---|---|
|
||||||
| **dépôts apt** | ce rôle — un cache, servi à la flotte |
|
| **dépôts apt** | ce rôle — un cache, servi à la flotte |
|
||||||
| **téléchargements directs** (Forgejo, Keycloak, Nextcloud et ses applications, oauth2-proxy) | ce rôle — le **dépôt de binaires** (`/var/lib/setops/artefacts-directs`, servi sous `/setops-binaires` sur le même port) |
|
| **téléchargements directs** (Forgejo, Keycloak, Nextcloud, oauth2-proxy, collections) | le **cache du contrôleur** : il télécharge une fois et pousse par SSH |
|
||||||
| **ce que le runner installe** (roues Python, collections Ansible, `verrou-runner.yml`) | ce rôle — le même dépôt de binaires |
|
|
||||||
|
|
||||||
Les rôles consommateurs prennent ces fichiers au dépôt du site d'abord, vérifient leur somme
|
Deux mécanismes parce que ce sont deux problèmes : on ne sert pas un dépôt apt par `scp`.
|
||||||
(ou leur signature), et ne sortent sur Internet qu'en repli, en le disant. Depuis le
|
|
||||||
2026-10-09, une reconstruction ne demande plus rien à PyPI, Galaxy ni à l'app store de
|
|
||||||
Nextcloud quand le dépôt est rempli.
|
|
||||||
|
|
||||||
## Ce qu'il ne couvre pas encore
|
## Ce qu'il ne couvre pas encore
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -172,7 +172,5 @@ serveur_artefacts_directs:
|
||||||
url: "{{ serveur_keycloak_signature_url }}"
|
url: "{{ serveur_keycloak_signature_url }}"
|
||||||
- nom: "nextcloud-{{ serveur_nextcloud_version }}.tar.bz2"
|
- nom: "nextcloud-{{ serveur_nextcloud_version }}.tar.bz2"
|
||||||
url: "{{ serveur_nextcloud_url }}"
|
url: "{{ serveur_nextcloud_url }}"
|
||||||
- nom: "nextcloud-{{ serveur_nextcloud_version }}.tar.bz2.asc"
|
|
||||||
url: "{{ serveur_nextcloud_signature_url }}"
|
|
||||||
- nom: "oauth2-proxy-{{ serveur_oauth2_proxy_version }}.tar.gz"
|
- nom: "oauth2-proxy-{{ serveur_oauth2_proxy_version }}.tar.gz"
|
||||||
url: "{{ serveur_oauth2_proxy_url }}"
|
url: "{{ serveur_oauth2_proxy_url }}"
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,10 @@
|
||||||
# Supervision derivee du role. Voir docs/supervision-conception.md.
|
# Supervision derivee du role. Voir docs/supervision-conception.md.
|
||||||
sondes:
|
sondes:
|
||||||
- nom: cache-apt
|
- nom: cache-apt
|
||||||
metier: paquets
|
|
||||||
ttl: 5400
|
ttl: 5400
|
||||||
raison: Le cache d'artefacts repond-il ? S'il se tait, TOUT apt de l'ecosysteme echoue — on agit dans
|
raison: Le cache d'artefacts repond-il ? S'il se tait, TOUT apt de l'ecosysteme echoue — on agit dans
|
||||||
la minute.
|
la minute.
|
||||||
- nom: cache-apt-volume
|
- nom: cache-apt-volume
|
||||||
metier: paquets
|
|
||||||
ttl: 5400
|
ttl: 5400
|
||||||
raison: 'Reste-t-il de la place au cache ? Un cache plein cesse d''ecrire sans se plaindre, et l''echec
|
raison: 'Reste-t-il de la place au cache ? Un cache plein cesse d''ecrire sans se plaindre, et l''echec
|
||||||
apparait chez les clients. Verdict distinct de la disponibilite : il appelle un billet, pas un reveil.'
|
apparait chez les clients. Verdict distinct de la disponibilite : il appelle un billet, pas un reveil.'
|
||||||
|
|
|
||||||
|
|
@ -199,14 +199,6 @@
|
||||||
label: "{{ item }}"
|
label: "{{ item }}"
|
||||||
when: serveur_artefacts_directs_actif | bool
|
when: serveur_artefacts_directs_actif | bool
|
||||||
|
|
||||||
# CE QUE LE RUNNER INSTALLE (2026-10-09) : ses roues Python et ses collections, verrouillees
|
|
||||||
# a l'octet dans `verrou-runner.yml`, a la racine du moteur. Lues, pas recopiees.
|
|
||||||
- name: Lire le verrou du runner (roues Python, collections Ansible)
|
|
||||||
ansible.builtin.include_vars:
|
|
||||||
file: "{{ role_path }}/../../verrou-runner.yml"
|
|
||||||
name: serveur_artefacts_verrou_runner
|
|
||||||
when: serveur_artefacts_directs_actif | bool
|
|
||||||
|
|
||||||
# `force: false` ET UN `dest` NOMME PAR LA VERSION : une publication ne change pas sous
|
# `force: false` ET UN `dest` NOMME PAR LA VERSION : une publication ne change pas sous
|
||||||
# le meme nom, donc un fichier deja present est le bon. Une nouvelle version porte un
|
# le meme nom, donc un fichier deja present est le bon. Une nouvelle version porte un
|
||||||
# autre nom et se telecharge d'elle-meme — le depot se met a jour en suivant les roles,
|
# autre nom et se telecharge d'elle-meme — le depot se met a jour en suivant les roles,
|
||||||
|
|
@ -219,18 +211,10 @@
|
||||||
ansible.builtin.get_url:
|
ansible.builtin.get_url:
|
||||||
url: "{{ item.url }}"
|
url: "{{ item.url }}"
|
||||||
dest: "{{ serveur_artefacts_directs_dir }}/{{ item.nom }}"
|
dest: "{{ serveur_artefacts_directs_dir }}/{{ item.nom }}"
|
||||||
checksum: "{{ ('sha256:' ~ item.sha256) if item.sha256 is defined else omit }}"
|
|
||||||
mode: "0644"
|
mode: "0644"
|
||||||
force: false
|
force: false
|
||||||
timeout: 60
|
timeout: 60
|
||||||
# Les applications de Nextcloud (2026-10-08) : declarees par `serveur_nextcloud`, lues
|
loop: "{{ serveur_artefacts_directs }}"
|
||||||
# telles quelles — meme `nom`, meme `url`, rien de recopie ici.
|
|
||||||
# Et le verrou du runner (2026-10-09). Un artefact qui declare sa somme est verifie ici :
|
|
||||||
# le site ne tient pas un fichier altere en chemin.
|
|
||||||
loop: >-
|
|
||||||
{{ serveur_artefacts_directs + (serveur_nextcloud_apps | default([]))
|
|
||||||
+ (serveur_artefacts_verrou_runner.python | default([]))
|
|
||||||
+ (serveur_artefacts_verrou_runner.collections | default([])) }}
|
|
||||||
loop_control:
|
loop_control:
|
||||||
label: "{{ item.nom }}"
|
label: "{{ item.nom }}"
|
||||||
register: serveur_artefacts_directs_tires
|
register: serveur_artefacts_directs_tires
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,6 @@
|
||||||
# peremption. Le silence alerte autant que l'echec.
|
# peremption. Le silence alerte autant que l'echec.
|
||||||
sondes:
|
sondes:
|
||||||
- nom: depot
|
- nom: depot
|
||||||
metier: donnees
|
|
||||||
ttl: 5400
|
ttl: 5400
|
||||||
raison: 'Le depot accepte-t-il encore une ecriture ? Il ne juge pas le contenu — chiffre
|
raison: 'Le depot accepte-t-il encore une ecriture ? Il ne juge pas le contenu — chiffre
|
||||||
cote client, il ne peut pas l''ouvrir. Il constate que l''endroit prend encore : en
|
cote client, il ne peut pas l''ouvrir. Il constate que l''endroit prend encore : en
|
||||||
|
|
|
||||||
|
|
@ -62,14 +62,9 @@ for noeud in {{ serveur_backup_noeuds_attendus | sort | join(' ') }}; do
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Le plus recent de tous les groupes (machine ET liste de chemins), pas le premier : voir
|
|
||||||
# `client_backup/templates/verifier-mon-depot.sh.j2` (2026-10-08).
|
|
||||||
json=$(restic -r "${depot}" snapshots --latest 1 --json 2>/dev/null)
|
json=$(restic -r "${depot}" snapshots --latest 1 --json 2>/dev/null)
|
||||||
horodatage=$(printf '%s' "${json}" | python3 -c \
|
horodatage=$(printf '%s' "${json}" | python3 -c \
|
||||||
'import sys,json,re,datetime
|
'import sys,json;d=json.load(sys.stdin);print(d[0]["time"] if d else "")' 2>/dev/null)
|
||||||
d=json.load(sys.stdin) or []
|
|
||||||
t=lambda x: datetime.datetime.fromisoformat(re.sub(r"Z$","+00:00",re.sub(r"(\.\d{6})\d+",r"\1",x["time"]))).timestamp()
|
|
||||||
print(max(d,key=t)["time"] if d else "")' 2>/dev/null)
|
|
||||||
if [[ -z "${horodatage}" ]]; then
|
if [[ -z "${horodatage}" ]]; then
|
||||||
rapporter "${noeud}" 2 "AUCUN INSTANTANE dans ${depot} — depot present mais vide."
|
rapporter "${noeud}" 2 "AUCUN INSTANTANE dans ${depot} — depot present mais vide."
|
||||||
continue
|
continue
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,6 @@
|
||||||
# peremption. Le silence alerte autant que l'echec.
|
# peremption. Le silence alerte autant que l'echec.
|
||||||
sondes:
|
sondes:
|
||||||
- nom: depot-locataires
|
- nom: depot-locataires
|
||||||
metier: sauvegardes_locataires
|
|
||||||
ttl: 5400
|
ttl: 5400
|
||||||
raison: 'Les depots des locataires sont-ils toujours etanches, et reste-t-il de la
|
raison: 'Les depots des locataires sont-ils toujours etanches, et reste-t-il de la
|
||||||
place devant eux ? Un mode qui glisse laisse un locataire lire ou effacer les
|
place devant eux ? Un mode qui glisse laisse un locataire lire ou effacer les
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,6 @@
|
||||||
# peremption. Le silence alerte autant que l'echec.
|
# peremption. Le silence alerte autant que l'echec.
|
||||||
sondes:
|
sondes:
|
||||||
- nom: cache-site-racine
|
- nom: cache-site-racine
|
||||||
metier: paquets
|
|
||||||
ttl: 5400
|
ttl: 5400
|
||||||
raison: 'Ce cache est-il toujours la RACINE de la chaine, et peut-il encore remplir ?
|
raison: 'Ce cache est-il toujours la RACINE de la chaine, et peut-il encore remplir ?
|
||||||
Un cache chaine sur lui-meme ou coupe de Debian repond parfaitement — il sert ce
|
Un cache chaine sur lui-meme ou coupe de Debian repond parfaitement — il sert ce
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@
|
||||||
# peremption. Le silence alerte autant que l'echec.
|
# peremption. Le silence alerte autant que l'echec.
|
||||||
sondes:
|
sondes:
|
||||||
- nom: edition
|
- nom: edition
|
||||||
metier: edition
|
|
||||||
ttl: 5400
|
ttl: 5400
|
||||||
raison: 'L''edition en ligne repond-elle ? `/hosting/discovery` est l''adresse que Nextcloud
|
raison: 'L''edition en ligne repond-elle ? `/hosting/discovery` est l''adresse que Nextcloud
|
||||||
interroge lui-meme pour savoir quels documents Collabora sait ouvrir. Sans elle, le
|
interroge lui-meme pour savoir quels documents Collabora sait ouvrir. Sans elle, le
|
||||||
|
|
|
||||||
|
|
@ -27,14 +27,12 @@
|
||||||
# correctif publie en exposition acceptee.
|
# correctif publie en exposition acceptee.
|
||||||
sondes:
|
sondes:
|
||||||
- nom: correctifs
|
- nom: correctifs
|
||||||
metier: socle
|
|
||||||
ttl: 5400
|
ttl: 5400
|
||||||
raison: >-
|
raison: >-
|
||||||
Des correctifs de securite attendent-ils d'etre appliques, et depuis quand ?
|
Des correctifs de securite attendent-ils d'etre appliques, et depuis quand ?
|
||||||
Set-OPS desarme les mises a jour automatiques et les applique au deploiement :
|
Set-OPS desarme les mises a jour automatiques et les applique au deploiement :
|
||||||
sans cette mesure, rien ne dit quand le geste est du.
|
sans cette mesure, rien ne dit quand le geste est du.
|
||||||
- nom: horloge
|
- nom: horloge
|
||||||
metier: socle
|
|
||||||
ttl: 5400
|
ttl: 5400
|
||||||
raison: >-
|
raison: >-
|
||||||
L'heure est-elle disciplinee, et par la SOURCE DECLAREE ? Une machine peut etre
|
L'heure est-elle disciplinee, et par la SOURCE DECLAREE ? Une machine peut etre
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
# Supervision derivee du role. Voir docs/supervision-conception.md.
|
# Supervision derivee du role. Voir docs/supervision-conception.md.
|
||||||
sondes:
|
sondes:
|
||||||
- nom: zones-publiques
|
- nom: zones-publiques
|
||||||
metier: noms_publics
|
|
||||||
ttl: 5400
|
ttl: 5400
|
||||||
raison: 'Le serveur public sert-il TOUTES les zones de ses locataires ? Un secondaire qui n''a jamais
|
raison: 'Le serveur public sert-il TOUTES les zones de ses locataires ? Un secondaire qui n''a jamais
|
||||||
recu une zone repond REFUSED : pour Internet, ce domaine n''a plus de serveur de noms, pendant que le
|
recu une zone repond REFUSED : pour Internet, ce domaine n''a plus de serveur de noms, pendant que le
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
# Supervision derivee du role. Voir docs/supervision-conception.md.
|
# Supervision derivee du role. Voir docs/supervision-conception.md.
|
||||||
sondes:
|
sondes:
|
||||||
- nom: boites
|
- nom: boites
|
||||||
metier: courriel
|
|
||||||
ttl: 5400
|
ttl: 5400
|
||||||
raison: 'Dovecot repond-il, et sait-il encore RESOUDRE un utilisateur dans l''annuaire ? Un Dovecot
|
raison: 'Dovecot repond-il, et sait-il encore RESOUDRE un utilisateur dans l''annuaire ? Un Dovecot
|
||||||
dont le lien LDAP est tombe accepte les connexions et refuse toutes les authentifications : le service
|
dont le lien LDAP est tombe accepte les connexions et refuse toutes les authentifications : le service
|
||||||
|
|
|
||||||
|
|
@ -51,41 +51,6 @@
|
||||||
group: "{{ serveur_dovecot_vmail_utilisateur }}"
|
group: "{{ serveur_dovecot_vmail_utilisateur }}"
|
||||||
mode: "0750"
|
mode: "0750"
|
||||||
|
|
||||||
# LES BOITES D'UNE INCARNATION PRECEDENTE (2026-09-30). Remises tant que le repertoire est
|
|
||||||
# vide — avant que la premiere livraison n'y cree quoi que ce soit.
|
|
||||||
- name: Restauration — les boites sont-elles vierges ?
|
|
||||||
ansible.builtin.find:
|
|
||||||
paths: "{{ serveur_dovecot_vmail_base }}"
|
|
||||||
file_type: any
|
|
||||||
register: serveur_dovecot_vmail_initial
|
|
||||||
|
|
||||||
- name: Restauration — serveur_dovecot d'une incarnation precedente ?
|
|
||||||
ansible.builtin.include_role:
|
|
||||||
name: client_backup
|
|
||||||
tasks_from: restaurer.yml
|
|
||||||
vars:
|
|
||||||
client_backup_restaurer_jeu: serveur_dovecot
|
|
||||||
client_backup_restaurer_vierge: "{{ serveur_dovecot_vmail_initial.matched | default(1) == 0 }}"
|
|
||||||
|
|
||||||
- name: Restauration — remettre serveur_dovecot
|
|
||||||
changed_when: true
|
|
||||||
ansible.builtin.command:
|
|
||||||
argv: >-
|
|
||||||
{{ ['/usr/local/sbin/setops-restaurer', 'fichiers',
|
|
||||||
'--instantane', client_backup_restauration.instantane]
|
|
||||||
+ ['--proprietaire', serveur_dovecot_vmail_utilisateur + ':' + serveur_dovecot_vmail_utilisateur]
|
|
||||||
+ client_backup_restauration.chemins }}
|
|
||||||
when: client_backup_restauration.etat == 'a_restaurer'
|
|
||||||
|
|
||||||
- name: Restauration — acter serveur_dovecot
|
|
||||||
ansible.builtin.include_role:
|
|
||||||
name: client_backup
|
|
||||||
tasks_from: acter.yml
|
|
||||||
vars:
|
|
||||||
client_backup_acter_etat: restaure
|
|
||||||
when: client_backup_restauration.etat == 'a_restaurer'
|
|
||||||
|
|
||||||
|
|
||||||
# --- Pont de certificat step_ca (déposé par client_pki) ---
|
# --- Pont de certificat step_ca (déposé par client_pki) ---
|
||||||
- name: TLS — créer le répertoire des certificats
|
- name: TLS — créer le répertoire des certificats
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@
|
||||||
# laisse la meme trace qu'un audit en bonne sante pour qui ne regarde que `systemctl`.
|
# laisse la meme trace qu'un audit en bonne sante pour qui ne regarde que `systemctl`.
|
||||||
sondes:
|
sondes:
|
||||||
- nom: audit
|
- nom: audit
|
||||||
metier: socle
|
|
||||||
ttl: 5400
|
ttl: 5400
|
||||||
# Posee par `auditd` seulement si `auditd_enabled` : Icinga ne l'attend qu'a ces
|
# Posee par `auditd` seulement si `auditd_enabled` : Icinga ne l'attend qu'a ces
|
||||||
# conditions (voir `setops-sondes.conf.j2`, 2026-09-28).
|
# conditions (voir `setops-sondes.conf.j2`, 2026-09-28).
|
||||||
|
|
@ -28,15 +27,3 @@ sondes:
|
||||||
La piste d'audit s'ecrit-elle vraiment ? Des regles armees dans le noyau ne
|
La piste d'audit s'ecrit-elle vraiment ? Des regles armees dans le noyau ne
|
||||||
prouvent pas qu'un demon collecte : le 2026-08-30, quinze machines ont eu les
|
prouvent pas qu'un demon collecte : le 2026-08-30, quinze machines ont eu les
|
||||||
deux apparences de la sante sans qu'une seule ligne soit ecrite.
|
deux apparences de la sante sans qu'une seule ligne soit ecrite.
|
||||||
- nom: connectivite
|
|
||||||
metier: socle
|
|
||||||
ttl: 180
|
|
||||||
# Attendue a la minute, et non au quart d'heure : c'est tout son interet. Voir le mode
|
|
||||||
# `minute` du porteur (`client_sante`).
|
|
||||||
fraicheur: 180
|
|
||||||
raison: >-
|
|
||||||
Chaque flux que le registre promet a cette machine passe-t-il, maintenant ? Un pare-feu
|
|
||||||
(Proxmox ou nftables) qui coupe un flux declare ne casse rien de visible tout de suite :
|
|
||||||
la sauvegarde echoue cette nuit, le certificat ne se renouvelle pas demain. Teste chaque
|
|
||||||
minute depuis la machine qui doit joindre, avec la cause (rejete, delai, personne
|
|
||||||
n'ecoute) — et les connexions entrantes que les regles declarees ne couvrent pas.
|
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,6 @@
|
||||||
# peremption. Le silence alerte autant que l'echec.
|
# peremption. Le silence alerte autant que l'echec.
|
||||||
sondes:
|
sondes:
|
||||||
- nom: genome-servi
|
- nom: genome-servi
|
||||||
metier: forge
|
|
||||||
ttl: 5400
|
ttl: 5400
|
||||||
raison: 'Cette forge a-t-elle encore un genome a servir ? Une forge vide repond
|
raison: 'Cette forge a-t-elle encore un genome a servir ? Une forge vide repond
|
||||||
parfaitement : son API marche, son Git est lisible, elle n''a simplement plus rien
|
parfaitement : son API marche, son Git est lisible, elle n''a simplement plus rien
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
---
|
---
|
||||||
# Version Forgejo : verifier/ajuster sur https://forgejo.org/download/
|
# Version Forgejo : verifier/ajuster sur https://forgejo.org/download/
|
||||||
serveur_forgejo_version: "16.0.5" # 2026-10-03 : trois corrections apres 16.0.2, meme branche
|
serveur_forgejo_version: "16.0.2"
|
||||||
serveur_forgejo_url: >-
|
serveur_forgejo_url: >-
|
||||||
https://codeberg.org/forgejo/forgejo/releases/download/v{{ serveur_forgejo_version }}/forgejo-{{ serveur_forgejo_version }}-linux-amd64
|
https://codeberg.org/forgejo/forgejo/releases/download/v{{ serveur_forgejo_version }}/forgejo-{{ serveur_forgejo_version }}-linux-amd64
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,17 +19,11 @@ flux:
|
||||||
# dépend ». Le vocabulaire des flux n'a pas de mot pour l'indécision, et c'est tant
|
# dépend ». Le vocabulaire des flux n'a pas de mot pour l'indécision, et c'est tant
|
||||||
# mieux : un flux se lit pour savoir ce qui circule et comment. Une valeur ambiguë
|
# mieux : un flux se lit pour savoir ce qui circule et comment. Une valeur ambiguë
|
||||||
# rendrait l'audit impossible sans aller lire le déploiement.
|
# rendrait l'audit impossible sans aller lire le déploiement.
|
||||||
#
|
|
||||||
# ET LA SITUATION SE LIT DANS LE REGLAGE (2026-09-30) : la premiere seulement si la forge ne
|
|
||||||
# sert pas son propre TLS. Au site (`serveur_forgejo_tls: true`), elle ecoute 443 et rien
|
|
||||||
# sur 3000 ; la regle restait posee parce que le site a un edge — et la sonde
|
|
||||||
# `connectivite` la montrait coupee chaque minute.
|
|
||||||
- sens: ingress
|
- sens: ingress
|
||||||
port: 3000
|
port: 3000
|
||||||
protocole: tcp
|
protocole: tcp
|
||||||
pair: edge
|
pair: edge
|
||||||
chiffrement: clair
|
chiffrement: clair
|
||||||
seulement_si: {variable: serveur_forgejo_tls, egal: false}
|
|
||||||
raison: >-
|
raison: >-
|
||||||
Interface web + Git HTTP derrière un edge : le nginx termine le TLS et parle en
|
Interface web + Git HTTP derrière un edge : le nginx termine le TLS et parle en
|
||||||
clair à la forge. C'est le cas de tout tenant.
|
clair à la forge. C'est le cas de tout tenant.
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
# Supervision derivee du role. Voir docs/supervision-conception.md.
|
# Supervision derivee du role. Voir docs/supervision-conception.md.
|
||||||
sondes:
|
sondes:
|
||||||
- nom: forge
|
- nom: forge
|
||||||
metier: forge
|
|
||||||
ttl: 5400
|
ttl: 5400
|
||||||
raison: La forge repond-elle a son API, et son depot Git est-il lisible ? Une forge qui sert sa page
|
raison: La forge repond-elle a son API, et son depot Git est-il lisible ? Une forge qui sert sa page
|
||||||
d'accueil mais ne peut plus lire ses depots est une panne complete pour tout ce qui clone — et elle
|
d'accueil mais ne peut plus lire ses depots est une panne complete pour tout ce qui clone — et elle
|
||||||
|
|
|
||||||
|
|
@ -272,57 +272,6 @@
|
||||||
dest: "{{ serveur_forgejo_binaire }}"
|
dest: "{{ serveur_forgejo_binaire }}"
|
||||||
state: link
|
state: link
|
||||||
notify: Redemarrer forgejo
|
notify: Redemarrer forgejo
|
||||||
# En simulation, une NOUVELLE version n'a pas ete copiee : le lien n'aurait pas de cible
|
|
||||||
# (2026-10-03, premiere montee simulee). Une version deja posee, elle, se simule.
|
|
||||||
when: not (ansible_check_mode and not serveur_forgejo_binaire_present.stat.exists)
|
|
||||||
|
|
||||||
# LES DEPOTS D'UNE INCARNATION PRECEDENTE (2026-09-30). Avant que le role ne cree
|
|
||||||
# l'arborescence : ensuite, le repertoire ne serait plus vierge. La base, elle, a deja
|
|
||||||
# ete remise par `serveur_postgresql` (ou vit dans ces fichiers, en SQLite). La
|
|
||||||
# configuration revient avec le secret JWT que le role relit plus bas.
|
|
||||||
- name: Restauration — les donnees de Forgejo sont-elles vierges ?
|
|
||||||
ansible.builtin.find:
|
|
||||||
paths: "{{ serveur_forgejo_data }}"
|
|
||||||
file_type: any
|
|
||||||
register: serveur_forgejo_data_initial
|
|
||||||
|
|
||||||
- name: Restauration — serveur_forgejo d'une incarnation precedente ?
|
|
||||||
ansible.builtin.include_role:
|
|
||||||
name: client_backup
|
|
||||||
tasks_from: restaurer.yml
|
|
||||||
vars:
|
|
||||||
client_backup_restaurer_jeu: serveur_forgejo
|
|
||||||
client_backup_restaurer_vierge: "{{ serveur_forgejo_data_initial.matched | default(1) == 0 }}"
|
|
||||||
|
|
||||||
- name: Restauration — remettre serveur_forgejo
|
|
||||||
changed_when: true
|
|
||||||
ansible.builtin.command:
|
|
||||||
argv: >-
|
|
||||||
{{ ['/usr/local/sbin/setops-restaurer', 'fichiers',
|
|
||||||
'--instantane', client_backup_restauration.instantane]
|
|
||||||
+ client_backup_restauration.chemins }}
|
|
||||||
when: client_backup_restauration.etat == 'a_restaurer'
|
|
||||||
|
|
||||||
# restic rend les proprietaires NUMERIQUES ; `git` n'a pas forcement le meme uid que sur
|
|
||||||
# la machine d'avant.
|
|
||||||
- name: Restauration — rendre les donnees au service
|
|
||||||
ansible.builtin.file:
|
|
||||||
path: "{{ item.chemin }}"
|
|
||||||
owner: "{{ item.proprietaire }}"
|
|
||||||
group: "{{ serveur_forgejo_utilisateur }}"
|
|
||||||
recurse: true
|
|
||||||
loop:
|
|
||||||
- { chemin: "{{ serveur_forgejo_data }}", proprietaire: "{{ serveur_forgejo_utilisateur }}" }
|
|
||||||
- { chemin: "{{ serveur_forgejo_config_dir }}", proprietaire: root }
|
|
||||||
when: client_backup_restauration.etat == 'a_restaurer'
|
|
||||||
|
|
||||||
- name: Restauration — acter serveur_forgejo
|
|
||||||
ansible.builtin.include_role:
|
|
||||||
name: client_backup
|
|
||||||
tasks_from: acter.yml
|
|
||||||
vars:
|
|
||||||
client_backup_acter_etat: restaure
|
|
||||||
when: client_backup_restauration.etat == 'a_restaurer'
|
|
||||||
|
|
||||||
- name: Creer les repertoires de donnees
|
- name: Creer les repertoires de donnees
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
# Supervision derivee du role. Voir docs/supervision-conception.md.
|
# Supervision derivee du role. Voir docs/supervision-conception.md.
|
||||||
sondes:
|
sondes:
|
||||||
- nom: tableaux
|
- nom: tableaux
|
||||||
metier: supervision
|
|
||||||
ttl: 5400
|
ttl: 5400
|
||||||
raison: 'Grafana repond-il, sa base tient-elle, et ses SOURCES DE DONNEES aussi ? Un Grafana
|
raison: 'Grafana repond-il, sa base tient-elle, et ses SOURCES DE DONNEES aussi ? Un Grafana
|
||||||
dont la base est tombee sert encore sa page de connexion ; un Grafana dont la source Loki
|
dont la base est tombee sert encore sa page de connexion ; un Grafana dont la source Loki
|
||||||
|
|
|
||||||
|
|
@ -335,21 +335,6 @@
|
||||||
when: setops_materiel is defined
|
when: setops_materiel is defined
|
||||||
notify: Redemarrer grafana
|
notify: Redemarrer grafana
|
||||||
|
|
||||||
# --- LA SYNTHESE (2026-10-04) — la porte d'entree, au site comme chez un locataire ------
|
|
||||||
#
|
|
||||||
# Le meme gabarit partout : il ajoute les hyperviseurs quand `setops_materiel` existe (au
|
|
||||||
# site), et ne voit que les machines de l'ecosysteme sinon. Pas de prefixe `setops-role-` :
|
|
||||||
# la moisson des tableaux orphelins ne doit pas le voir.
|
|
||||||
- name: Assembler le tableau de synthèse
|
|
||||||
ansible.builtin.template:
|
|
||||||
src: tableau-synthese.json.j2
|
|
||||||
dest: "{{ serveur_grafana_dashboards_dir }}/setops-synthese.json"
|
|
||||||
owner: root
|
|
||||||
group: grafana
|
|
||||||
mode: "0640"
|
|
||||||
validate: "python3 -c \"import json,sys; json.load(open(sys.argv[1]))\" %s"
|
|
||||||
notify: Redemarrer grafana
|
|
||||||
|
|
||||||
# --- UN ROLE QU'ON RETIRE DOIT POUVOIR DEFAIRE CE QU'IL A FAIT ------------------------
|
# --- UN ROLE QU'ON RETIRE DOIT POUVOIR DEFAIRE CE QU'IL A FAIT ------------------------
|
||||||
#
|
#
|
||||||
# Meme lecon que les sondes orphelines de `client_sante`, et elle s'est deja payee ici :
|
# Meme lecon que les sondes orphelines de `client_sante`, et elle s'est deja payee ici :
|
||||||
|
|
|
||||||
|
|
@ -11,10 +11,6 @@ Environment=GF_SERVER_DOMAIN={{ serveur_grafana_hostname }}
|
||||||
Environment=GF_SERVER_ROOT_URL={{ serveur_grafana_root_url }}
|
Environment=GF_SERVER_ROOT_URL={{ serveur_grafana_root_url }}
|
||||||
Environment=GF_SECURITY_ADMIN_PASSWORD={{ serveur_grafana_admin_password }}
|
Environment=GF_SECURITY_ADMIN_PASSWORD={{ serveur_grafana_admin_password }}
|
||||||
Environment=GF_USERS_ALLOW_SIGN_UP=false
|
Environment=GF_USERS_ALLOW_SIGN_UP=false
|
||||||
{# LA SYNTHESE COMME PAGE D'ACCUEIL (2026-10-04) : a la connexion, la question « faut-il
|
|
||||||
agir ? » plutot que la liste des tableaux. Le fichier est pose par ce role (tableau de
|
|
||||||
synthese) ; sans lui, Grafana revient a son accueil par defaut, sans erreur. #}
|
|
||||||
Environment=GF_DASHBOARDS_DEFAULT_HOME_DASHBOARD_PATH={{ serveur_grafana_dashboards_dir }}/setops-synthese.json
|
|
||||||
{# Le formulaire local est offert, ou il ne l'est pas — et le gabarit le DIT dans
|
{# Le formulaire local est offert, ou il ne l'est pas — et le gabarit le DIT dans
|
||||||
les deux cas. Cette ligne vivait sous le `if` du SSO : SSO eteint, elle
|
les deux cas. Cette ligne vivait sous le `if` du SSO : SSO eteint, elle
|
||||||
disparaissait, et c'est le defaut de Grafana qui decidait en silence. Un
|
disparaissait, et c'est le defaut de Grafana qui decidait en silence. Un
|
||||||
|
|
|
||||||
|
|
@ -21,30 +21,6 @@
|
||||||
{"color": "red", "value": f.crit}]} | tojson }}
|
{"color": "red", "value": f.crit}]} | tojson }}
|
||||||
{%- endmacro -%}
|
{%- endmacro -%}
|
||||||
|
|
||||||
{#- LES PHRASES DES PANNEAUX (2026-10-04). Trois temps, toujours les memes : ce que ca
|
|
||||||
mesure (`raison`), ou sont les seuils, et QUOI FAIRE quand la couleur change (`action`,
|
|
||||||
dans le catalogue). Grafana les montre au survol du titre (icone i), en Markdown. -#}
|
|
||||||
{%- macro desc_temp(f) -%}
|
|
||||||
{{ f.raison ~ "\n\n**Seuils** : orange à " ~ f.avert ~ " °C, rouge à " ~ f.crit ~ " °C — le verdict Icinga est lissé sur " ~ (f.jugee_sur | replace("m", " min")) ~ " : un pic d'une seconde n'est pas une panne.\n\n**Si c'est orange ou rouge** : " ~ f.action }}
|
|
||||||
{%- endmacro -%}
|
|
||||||
{%- macro desc_ctrl(c) -%}
|
|
||||||
{{ c.raison ~ "\n\n**Si la tuile n'affiche pas 0** : " ~ c.action }}
|
|
||||||
{%- endmacro -%}
|
|
||||||
|
|
||||||
{#- ---- MODE D'EMPLOI ---------------------------------------------------------------- #}
|
|
||||||
{%- set y.id = y.id + 1 -%}
|
|
||||||
{%- set _ = panneaux.append({
|
|
||||||
"type": "text", "id": y.id, "title": "",
|
|
||||||
"gridPos": {"h": 4, "w": 24, "x": 0, "y": y.v},
|
|
||||||
"options": {"mode": "markdown", "content":
|
|
||||||
"**Comment lire ce tableau.** Chaque rangée répond à une question.\n\n"
|
|
||||||
~ "- **État en direct** — y a-t-il quelque chose à faire maintenant ? Vert : rien. Orange : à planifier. Rouge : à traiter.\n"
|
|
||||||
~ "- **Températures** — depuis quand, et sur quelle machine ? Les pointillés sont les seuils.\n"
|
|
||||||
~ "- **Ventilateurs et puissance** — une chaleur vient-elle de la charge ou du refroidissement ?\n"
|
|
||||||
~ "- **Stockage** — quels disques vieillissent, et lesquels se dégradent ?\n\n"
|
|
||||||
~ "Survoler le **titre** d'un panneau (icône ⓘ) : il dit ce qu'il mesure, pourquoi cela compte, et **quoi faire** s'il change de couleur. Seuils identiques à ceux du service Icinga « materiel »."}}) -%}
|
|
||||||
{%- set y.v = y.v + 4 -%}
|
|
||||||
|
|
||||||
{#- ---- RANGEE : ETAT EN DIRECT ------------------------------------------------------ #}
|
{#- ---- RANGEE : ETAT EN DIRECT ------------------------------------------------------ #}
|
||||||
{%- set y.id = y.id + 1 -%}
|
{%- set y.id = y.id + 1 -%}
|
||||||
{%- set _ = panneaux.append({"type": "row", "id": y.id, "title": "État en direct", "collapsed": false,
|
{%- set _ = panneaux.append({"type": "row", "id": y.id, "title": "État en direct", "collapsed": false,
|
||||||
|
|
@ -56,7 +32,7 @@
|
||||||
{%- set _ = panneaux.append({
|
{%- set _ = panneaux.append({
|
||||||
"type": "stat", "id": y.id,
|
"type": "stat", "id": y.id,
|
||||||
"title": f.titre ~ " — maximum",
|
"title": f.titre ~ " — maximum",
|
||||||
"description": desc_temp(f),
|
"description": f.raison,
|
||||||
"datasource": {"type": "prometheus", "uid": "${ds_prom}"},
|
"datasource": {"type": "prometheus", "uid": "${ds_prom}"},
|
||||||
"gridPos": {"h": 5, "w": 6, "x": loop.index0 * 6, "y": y.v},
|
"gridPos": {"h": 5, "w": 6, "x": loop.index0 * 6, "y": y.v},
|
||||||
"fieldConfig": {"defaults": {"unit": "celsius", "decimals": 0,
|
"fieldConfig": {"defaults": {"unit": "celsius", "decimals": 0,
|
||||||
|
|
@ -77,7 +53,7 @@
|
||||||
{%- set _ = panneaux.append({
|
{%- set _ = panneaux.append({
|
||||||
"type": "stat", "id": y.id,
|
"type": "stat", "id": y.id,
|
||||||
"title": c.titre,
|
"title": c.titre,
|
||||||
"description": desc_ctrl(c),
|
"description": c.raison,
|
||||||
"datasource": {"type": "prometheus", "uid": "${ds_prom}"},
|
"datasource": {"type": "prometheus", "uid": "${ds_prom}"},
|
||||||
"gridPos": {"h": 4, "w": largeur, "x": (loop.index0 * largeur) % 24, "y": y.v + ((loop.index0 * largeur) // 24) * 4},
|
"gridPos": {"h": 4, "w": largeur, "x": (loop.index0 * largeur) % 24, "y": y.v + ((loop.index0 * largeur) // 24) * 4},
|
||||||
"fieldConfig": {"defaults": {"unit": "none", "decimals": 0,
|
"fieldConfig": {"defaults": {"unit": "none", "decimals": 0,
|
||||||
|
|
@ -104,7 +80,7 @@
|
||||||
{%- set _ = panneaux.append({
|
{%- set _ = panneaux.append({
|
||||||
"type": "timeseries", "id": y.id,
|
"type": "timeseries", "id": y.id,
|
||||||
"title": f.titre,
|
"title": f.titre,
|
||||||
"description": desc_temp(f),
|
"description": f.raison ~ " — Avertissement " ~ f.avert ~ " °C, critique " ~ f.crit ~ " °C (verdict Icinga lisse sur " ~ f.jugee_sur ~ ").",
|
||||||
"datasource": {"type": "prometheus", "uid": "${ds_prom}"},
|
"datasource": {"type": "prometheus", "uid": "${ds_prom}"},
|
||||||
"gridPos": {"h": 9, "w": 12, "x": 0 if loop.index0 % 2 == 0 else 12, "y": y.v + (loop.index0 // 2) * 9},
|
"gridPos": {"h": 9, "w": 12, "x": 0 if loop.index0 % 2 == 0 else 12, "y": y.v + (loop.index0 // 2) * 9},
|
||||||
"fieldConfig": {"defaults": {
|
"fieldConfig": {"defaults": {
|
||||||
|
|
@ -131,7 +107,7 @@
|
||||||
{%- set y.id = y.id + 1 -%}
|
{%- set y.id = y.id + 1 -%}
|
||||||
{%- set _ = panneaux.append({
|
{%- set _ = panneaux.append({
|
||||||
"type": "timeseries", "id": y.id, "title": "Ventilateurs",
|
"type": "timeseries", "id": y.id, "title": "Ventilateurs",
|
||||||
"description": "Vitesse de chaque ventilateur, en tours par minute. Seuls les connecteurs qui ont tourné dans les dernières 24 h sont tracés : un connecteur vide lit 0 depuis toujours et n'apprend rien.\n\n**À lire avec les températures** : une température qui monte pendant que les ventilateurs restent bas parle d'une courbe de ventilation trop douce ; une courbe qui tombe à 0, d'un ventilateur arrêté (tuile rouge plus haut).",
|
"description": "Seuls les connecteurs qui ont tourne dans les dernieres 24 h : un connecteur vide lit 0 depuis toujours et n'apprend rien.",
|
||||||
"datasource": {"type": "prometheus", "uid": "${ds_prom}"},
|
"datasource": {"type": "prometheus", "uid": "${ds_prom}"},
|
||||||
"gridPos": {"h": 9, "w": 12, "x": 0, "y": y.v},
|
"gridPos": {"h": 9, "w": 12, "x": 0, "y": y.v},
|
||||||
"fieldConfig": {"defaults": {"unit": "rotrpm", "decimals": 0, "color": {"mode": "palette-classic"},
|
"fieldConfig": {"defaults": {"unit": "rotrpm", "decimals": 0, "color": {"mode": "palette-classic"},
|
||||||
|
|
@ -145,7 +121,7 @@
|
||||||
{%- set y.id = y.id + 1 -%}
|
{%- set y.id = y.id + 1 -%}
|
||||||
{%- set _ = panneaux.append({
|
{%- set _ = panneaux.append({
|
||||||
"type": "timeseries", "id": y.id, "title": "Puissance du processeur (PPT)",
|
"type": "timeseries", "id": y.id, "title": "Puissance du processeur (PPT)",
|
||||||
"description": "Puissance du processeur (PPT), annoncée par le pilote amdgpu de l'APU intégré. Ce n'est pas la consommation de la machine entière : aucune mesure ne la donne.\n\n**À lire avec les températures** : puissance et température qui montent ensemble, c'est la charge. Une température qui monte **sans** que la puissance monte, c'est le refroidissement : ventilateurs, poussière, pâte thermique.",
|
"description": "Puissance du boitier processeur annoncee par le pilote amdgpu (APU integre). Elle suit la charge : une temperature qui monte SANS puissance qui monte parle du refroidissement.",
|
||||||
"datasource": {"type": "prometheus", "uid": "${ds_prom}"},
|
"datasource": {"type": "prometheus", "uid": "${ds_prom}"},
|
||||||
"gridPos": {"h": 9, "w": 12, "x": 12, "y": y.v},
|
"gridPos": {"h": 9, "w": 12, "x": 12, "y": y.v},
|
||||||
"fieldConfig": {"defaults": {"unit": "watt", "decimals": 1, "color": {"mode": "palette-classic"},
|
"fieldConfig": {"defaults": {"unit": "watt", "decimals": 1, "color": {"mode": "palette-classic"},
|
||||||
|
|
@ -166,7 +142,7 @@
|
||||||
{%- set y.id = y.id + 1 -%}
|
{%- set y.id = y.id + 1 -%}
|
||||||
{%- set _ = panneaux.append({
|
{%- set _ = panneaux.append({
|
||||||
"type": "bargauge", "id": y.id, "title": "Usure des NVMe",
|
"type": "bargauge", "id": y.id, "title": "Usure des NVMe",
|
||||||
"description": "Part de l'endurance garantie déjà consommée par chaque NVMe, selon le disque lui-même.\n\n**Seuils** : orange à 80 %, rouge à 90 %.\n\n**Si c'est orange** : commander un remplaçant, rien ne presse encore. **Si c'est rouge** : planifier le remplacement ; pour un OSD Ceph, le sortir du cluster avant.",
|
"description": "Part de l'endurance annoncee deja consommee. Avertissement a 80 % : a commander, pas encore a remplacer.",
|
||||||
"datasource": {"type": "prometheus", "uid": "${ds_prom}"},
|
"datasource": {"type": "prometheus", "uid": "${ds_prom}"},
|
||||||
"gridPos": {"h": 9, "w": 8, "x": 0, "y": y.v},
|
"gridPos": {"h": 9, "w": 8, "x": 0, "y": y.v},
|
||||||
"fieldConfig": {"defaults": {"unit": "percentunit", "min": 0, "max": 1, "decimals": 0,
|
"fieldConfig": {"defaults": {"unit": "percentunit", "min": 0, "max": 1, "decimals": 0,
|
||||||
|
|
@ -180,7 +156,7 @@
|
||||||
{%- set y.id = y.id + 1 -%}
|
{%- set y.id = y.id + 1 -%}
|
||||||
{%- set _ = panneaux.append({
|
{%- set _ = panneaux.append({
|
||||||
"type": "bargauge", "id": y.id, "title": "Secteurs défaillants",
|
"type": "bargauge", "id": y.id, "title": "Secteurs défaillants",
|
||||||
"description": "Secteurs en attente, irréparables et réalloués, additionnés par disque SATA.\n\n**Un compte stable se surveille, un compte qui monte se remplace** : la tuile « secteurs en progression » passe au rouge dès qu'il augmente.\n\n**Si ce n'est pas 0** : prévoir le remplacement ; pour un OSD Ceph, le sortir du cluster (« out ») avant que la dégradation s'accélère.",
|
"description": "En attente + irreparables + realloues, par disque SATA. Un compte stable se surveille ; un compte qui MONTE se remplace (controle « secteurs en progression »).",
|
||||||
"datasource": {"type": "prometheus", "uid": "${ds_prom}"},
|
"datasource": {"type": "prometheus", "uid": "${ds_prom}"},
|
||||||
"gridPos": {"h": 9, "w": 8, "x": 8, "y": y.v},
|
"gridPos": {"h": 9, "w": 8, "x": 8, "y": y.v},
|
||||||
"fieldConfig": {"defaults": {"unit": "none", "min": 0, "decimals": 0,
|
"fieldConfig": {"defaults": {"unit": "none", "min": 0, "decimals": 0,
|
||||||
|
|
@ -194,7 +170,7 @@
|
||||||
{%- set y.id = y.id + 1 -%}
|
{%- set y.id = y.id + 1 -%}
|
||||||
{%- set _ = panneaux.append({
|
{%- set _ = panneaux.append({
|
||||||
"type": "bargauge", "id": y.id, "title": "Heures de fonctionnement",
|
"type": "bargauge", "id": y.id, "title": "Heures de fonctionnement",
|
||||||
"description": "Âge réel de chaque disque, en heures sous tension.\n\n**Seuil** : orange au-delà de 40 000 heures (4,5 ans), quand un disque mécanique entre dans la zone où les pannes se multiplient.\n\n**Si c'est orange** : en prévoir le remplacement dans le budget, surtout s'il porte aussi des secteurs défaillants.",
|
"description": "Age reel des disques. Un disque mecanique au-dela de 40 000 heures (4,5 ans) entre dans la zone ou les pannes se multiplient.",
|
||||||
"datasource": {"type": "prometheus", "uid": "${ds_prom}"},
|
"datasource": {"type": "prometheus", "uid": "${ds_prom}"},
|
||||||
"gridPos": {"h": 9, "w": 8, "x": 16, "y": y.v},
|
"gridPos": {"h": 9, "w": 8, "x": 16, "y": y.v},
|
||||||
"fieldConfig": {"defaults": {"unit": "h", "min": 0, "decimals": 0,
|
"fieldConfig": {"defaults": {"unit": "h", "min": 0, "decimals": 0,
|
||||||
|
|
@ -209,7 +185,7 @@
|
||||||
{{ {
|
{{ {
|
||||||
"uid": "setops-materiel",
|
"uid": "setops-materiel",
|
||||||
"title": "Set-OPS — Matériel",
|
"title": "Set-OPS — Matériel",
|
||||||
"description": "Hyperviseurs et frontière : températures, ventilateurs, puissance, disques. Même catalogue et mêmes seuils que le service Icinga « materiel » (scripts/materiel.py).",
|
"description": "Hyperviseurs et frontiere : temperatures, ventilateurs, puissance, disques. Meme catalogue et memes seuils que le service Icinga « materiel » (scripts/materiel.py).",
|
||||||
"tags": ["set-ops", "derive", "materiel", "fabric"],
|
"tags": ["set-ops", "derive", "materiel", "fabric"],
|
||||||
"timezone": "browser",
|
"timezone": "browser",
|
||||||
"refresh": "1m",
|
"refresh": "1m",
|
||||||
|
|
|
||||||
|
|
@ -1,216 +0,0 @@
|
||||||
{#- GENERE par Set-OPS (role serveur_grafana). Ne pas editer a la main.
|
|
||||||
|
|
||||||
LE TABLEAU DE SYNTHESE (2026-10-04) — la porte d'entree de l'observatoire.
|
|
||||||
|
|
||||||
Quatre rangees, quatre questions : faut-il agir ? reste-t-il de la marge ? que se
|
|
||||||
passe-t-il ? ou regarder ensuite ? Chaque panneau dit ce qu'il mesure, ou sont ses
|
|
||||||
seuils, et QUOI FAIRE quand sa couleur change — meme patron que le tableau Materiel.
|
|
||||||
|
|
||||||
LE MEME GABARIT AU SITE ET CHEZ UN LOCATAIRE. Au site, `setops_materiel` existe : la
|
|
||||||
synthese ajoute les hyperviseurs (tuile materiel, memoire et swap) et regarde leur
|
|
||||||
activite. Chez un locataire, elle ne voit que ses propres machines.
|
|
||||||
|
|
||||||
LE NOM DES MACHINES vient de `node_uname_info` (joint sur `instance`) : les locataires
|
|
||||||
n'ont pas l'etiquette `hote` du site, et une adresse IP ne parle a personne.
|
|
||||||
-#}
|
|
||||||
{%- set fabric = setops_materiel is defined -%}
|
|
||||||
{#- La tuile materiel rend un NIVEAU (0, 1, 2), pas un compte : `count(x) > 0` garde la
|
|
||||||
valeur du compte — trois secteurs en avertissement s'affichaient « 3 ». -#}
|
|
||||||
{%- set cat = setops_materiel | default({"temperatures": [], "controles": []}) -%}
|
|
||||||
{%- set nom = ' * on(instance) group_left(nodename) node_uname_info' -%}
|
|
||||||
{%- set ds = {"type": "prometheus", "uid": "${ds_prom}"} -%}
|
|
||||||
{%- set panneaux = [] -%}
|
|
||||||
{%- set y = namespace(v=0, id=0) -%}
|
|
||||||
|
|
||||||
{#- ---- MODE D'EMPLOI ---------------------------------------------------------------- #}
|
|
||||||
{%- set y.id = y.id + 1 -%}
|
|
||||||
{%- set _ = panneaux.append({
|
|
||||||
"type": "text", "id": y.id, "title": "",
|
|
||||||
"gridPos": {"h": 3, "w": 24, "x": 0, "y": y.v},
|
|
||||||
"options": {"mode": "markdown", "content":
|
|
||||||
"**Synthèse " ~ ("du site" if fabric else "de l'écosystème") ~ ".** Quatre rangées, quatre questions : **faut-il agir ?** — **reste-t-il de la marge ?** — **que se passe-t-il ?** — **où regarder ensuite ?** "
|
|
||||||
~ "Vert : rien à faire. Orange : à planifier. Rouge : à traiter. Survoler le titre d'un panneau (ⓘ) dit ce qu'il mesure et quoi faire s'il change de couleur."}}) -%}
|
|
||||||
{%- set y.v = y.v + 3 -%}
|
|
||||||
|
|
||||||
{#- ---- RANGEE 1 : FAUT-IL AGIR ? ----------------------------------------------------- #}
|
|
||||||
{%- set y.id = y.id + 1 -%}
|
|
||||||
{%- set _ = panneaux.append({"type": "row", "id": y.id, "title": "Faut-il agir ?", "collapsed": false,
|
|
||||||
"gridPos": {"h": 1, "w": 24, "x": 0, "y": y.v}}) -%}
|
|
||||||
{%- set y.v = y.v + 1 -%}
|
|
||||||
|
|
||||||
{%- set tuiles = [] -%}
|
|
||||||
{%- if fabric -%}
|
|
||||||
{%- set crit = [] -%}
|
|
||||||
{%- set avert = [] -%}
|
|
||||||
{%- for c in cat.controles -%}
|
|
||||||
{%- set _ = (crit if c.niveau == "crit" else avert).append(c.expr) -%}
|
|
||||||
{%- endfor -%}
|
|
||||||
{%- for f in cat.temperatures -%}
|
|
||||||
{%- if f.crit is not none -%}{%- set _ = crit.append("(" ~ f.expr_juge ~ ") > " ~ f.crit) -%}{%- endif -%}
|
|
||||||
{%- if f.avert is not none -%}{%- set _ = avert.append("(" ~ f.expr_juge ~ ") > " ~ f.avert) -%}{%- endif -%}
|
|
||||||
{%- endfor -%}
|
|
||||||
{%- set _ = tuiles.append({
|
|
||||||
"titre": "Matériel des hyperviseurs",
|
|
||||||
"expr": "(count((" ~ (crit | join(") or (")) ~ ")) > 0) * 0 + 2 or (count((" ~ (avert | join(") or (")) ~ ")) > 0) * 0 + 1 or vector(0)",
|
|
||||||
"unite": "none",
|
|
||||||
"seuils": [{"color": "green", "value": none}, {"color": "orange", "value": 1}, {"color": "red", "value": 2}],
|
|
||||||
"mappings": [{"type": "value", "options": {"0": {"text": "Rien à faire"}, "1": {"text": "À planifier"}, "2": {"text": "À traiter"}}}],
|
|
||||||
"desc": "Le pire verdict du tableau Matériel : températures et contrôles des disques, ventilateurs et NVMe, avec les mêmes seuils que le service Icinga « materiel ».\n\n**Si c'est orange ou rouge** : ouvrir le tableau « Set-OPS — Matériel » ; la tuile colorée y dit quel équipement, et sa description dit quoi faire."}) -%}
|
|
||||||
{%- set _ = tuiles.append({
|
|
||||||
"titre": "Swap le plus plein (hyperviseurs)",
|
|
||||||
"expr": "max(100 * (1 - node_memory_SwapFree_bytes{job=\"fabric\"} / node_memory_SwapTotal_bytes{job=\"fabric\"}))",
|
|
||||||
"unite": "percent",
|
|
||||||
"seuils": [{"color": "green", "value": none}, {"color": "orange", "value": 50}, {"color": "red", "value": 90}],
|
|
||||||
"desc": "Le swap le plus rempli des trois hyperviseurs. Un hyperviseur qui swappe ralentit toutes les VM qu'il porte, sans aucune erreur visible.\n\n**Seuils** : orange à 50 %, rouge à 90 %.\n\n**Si c'est orange ou rouge** : la mémoire allouée aux VM dépasse ce que la machine peut tenir. Réduire la RAM des VM qui en ont trop (voir « Mémoire utilisée par machine »), arrêter les VM inutiles, ou déplacer une VM vers l'hyperviseur le moins chargé."}) -%}
|
|
||||||
{%- endif -%}
|
|
||||||
{%- set _ = tuiles.append({
|
|
||||||
"titre": "Disque le plus plein (machines)",
|
|
||||||
"expr": "max(100 * (1 - node_filesystem_avail_bytes{job=\"node\",mountpoint=\"/\"} / node_filesystem_size_bytes{job=\"node\",mountpoint=\"/\"}))",
|
|
||||||
"unite": "percent",
|
|
||||||
"seuils": [{"color": "green", "value": none}, {"color": "orange", "value": 80}, {"color": "red", "value": 90}],
|
|
||||||
"desc": "Le disque système le plus rempli parmi les machines de cet écosystème.\n\n**Seuils** : orange à 80 %, rouge à 90 % — les mêmes que la sonde Icinga « disque ».\n\n**Si c'est orange ou rouge** : trouver la machine dans « Disque utilisé par machine », plus bas ; chercher ce qui grossit (journaux, cache apt, sauvegardes locales) ou agrandir son disque."}) -%}
|
|
||||||
{%- set _ = tuiles.append({
|
|
||||||
"titre": "Correctifs de sécurité en attente",
|
|
||||||
"expr": "sum(apt_upgrades_pending{origin=~\"(?i).*securit.*\"}) or vector(0)",
|
|
||||||
"unite": "none",
|
|
||||||
"seuils": [{"color": "green", "value": none}, {"color": "orange", "value": 1}],
|
|
||||||
"desc": "Nombre de paquets de sécurité Debian prêts à être installés, toutes machines confondues" ~ (" (hyperviseurs compris)" if fabric else "") ~ ".\n\n**Seuil** : orange dès 1.\n\n**Si c'est orange** : les machines du socle les appliquent d'elles-mêmes dans la nuit ; si le compte ne redescend pas d'ici le lendemain, la sonde Icinga « correctifs » dit lesquelles bloquent." ~ (" Les hyperviseurs, eux, se mettent à jour à la main." if fabric else "")}) -%}
|
|
||||||
{%- set _ = tuiles.append({
|
|
||||||
"titre": "Redémarrages en attente",
|
|
||||||
"expr": "count(node_reboot_required == 1) or vector(0)",
|
|
||||||
"unite": "none",
|
|
||||||
"seuils": [{"color": "green", "value": none}, {"color": "orange", "value": 1}],
|
|
||||||
"desc": "Machines dont une mise à jour (noyau, bibliothèque système) ne prendra effet qu'au prochain redémarrage.\n\n**Seuil** : orange dès 1.\n\n**Si c'est orange** : planifier les redémarrages, une machine à la fois, en dehors des heures d'usage. Tant qu'elles ne redémarrent pas, le correctif est installé mais pas actif."}) -%}
|
|
||||||
{%- set _ = tuiles.append({
|
|
||||||
"titre": "Machines muettes",
|
|
||||||
"expr": "count(up == 0) or vector(0)",
|
|
||||||
"unite": "none",
|
|
||||||
"seuils": [{"color": "green", "value": none}, {"color": "red", "value": 1}],
|
|
||||||
"desc": "Cibles que Prometheus n'arrive plus à interroger : machine arrêtée, exportateur tombé, ou flux coupé.\n\n**Seuil** : rouge dès 1.\n\n**Si c'est rouge** : ce tableau ne voit plus cette machine, et tous ses chiffres sont figés. Vérifier qu'elle tourne, puis la sonde Icinga « collecte », qui la nomme."}) -%}
|
|
||||||
|
|
||||||
{%- set largeur = 24 // (tuiles | length) -%}
|
|
||||||
{%- for t in tuiles -%}
|
|
||||||
{%- set y.id = y.id + 1 -%}
|
|
||||||
{%- set _ = panneaux.append({
|
|
||||||
"type": "stat", "id": y.id, "title": t.titre, "description": t.desc, "datasource": ds,
|
|
||||||
"gridPos": {"h": 5, "w": largeur, "x": loop.index0 * largeur, "y": y.v},
|
|
||||||
"fieldConfig": {"defaults": {"unit": t.unite, "decimals": 0, "noValue": "—",
|
|
||||||
"mappings": t.mappings | default([]),
|
|
||||||
"thresholds": {"mode": "absolute", "steps": t.seuils}}, "overrides": []},
|
|
||||||
"options": {"colorMode": "background", "graphMode": "none", "justifyMode": "center",
|
|
||||||
"textMode": "value", "orientation": "auto",
|
|
||||||
"reduceOptions": {"calcs": ["lastNotNull"], "fields": "", "values": false}},
|
|
||||||
"targets": [{"refId": "A", "datasource": ds, "expr": t.expr}]}) -%}
|
|
||||||
{%- endfor -%}
|
|
||||||
{%- set y.v = y.v + 5 -%}
|
|
||||||
|
|
||||||
{#- ---- RANGEE 2 : RESTE-T-IL DE LA MARGE ? ----------------------------------------- #}
|
|
||||||
{%- set y.id = y.id + 1 -%}
|
|
||||||
{%- set _ = panneaux.append({"type": "row", "id": y.id, "title": "Reste-t-il de la marge ?", "collapsed": false,
|
|
||||||
"gridPos": {"h": 1, "w": 24, "x": 0, "y": y.v}}) -%}
|
|
||||||
{%- set y.v = y.v + 1 -%}
|
|
||||||
{%- set w = 8 if fabric else 12 -%}
|
|
||||||
{%- set x = namespace(v=0) -%}
|
|
||||||
{%- if fabric -%}
|
|
||||||
{%- set y.id = y.id + 1 -%}
|
|
||||||
{%- set _ = panneaux.append({
|
|
||||||
"type": "timeseries", "id": y.id, "title": "Mémoire et swap des hyperviseurs", "datasource": ds,
|
|
||||||
"description": "Pour chaque hyperviseur : la part de sa RAM utilisée, et celle de son swap.\n\n**Comment le lire** : une RAM plate autour de 80 % avec un swap qui monte veut dire que les VM réclament plus que la machine n'a. Le swap n'est pas une marge, c'est un ralentissement.\n\n**Seuils** (pointillés) : 80 % et 90 %.\n\n**Si une courbe de swap reste haute** : réduire la RAM allouée aux VM de cet hyperviseur, ou en déplacer une.",
|
|
||||||
"gridPos": {"h": 9, "w": w, "x": x.v, "y": y.v},
|
|
||||||
"fieldConfig": {"defaults": {"unit": "percent", "min": 0, "max": 100, "decimals": 0, "color": {"mode": "palette-classic"},
|
|
||||||
"thresholds": {"mode": "absolute", "steps": [{"color": "green", "value": none}, {"color": "orange", "value": 80}, {"color": "red", "value": 90}]},
|
|
||||||
"custom": {"drawStyle": "line", "lineWidth": 2, "fillOpacity": 5, "showPoints": "never", "spanNulls": true, "thresholdsStyle": {"mode": "dashed"}}},
|
|
||||||
"overrides": []},
|
|
||||||
"options": {"legend": {"displayMode": "table", "placement": "bottom", "calcs": ["lastNotNull", "max"]}, "tooltip": {"mode": "multi", "sort": "desc"}},
|
|
||||||
"targets": [
|
|
||||||
{"refId": "A", "datasource": ds, "legendFormat": "{{nodename}} · RAM",
|
|
||||||
"expr": "100 * (1 - node_memory_MemAvailable_bytes{job=\"fabric\"} / node_memory_MemTotal_bytes{job=\"fabric\"})" ~ nom},
|
|
||||||
{"refId": "B", "datasource": ds, "legendFormat": "{{nodename}} · swap",
|
|
||||||
"expr": "100 * (1 - node_memory_SwapFree_bytes{job=\"fabric\"} / node_memory_SwapTotal_bytes{job=\"fabric\"})" ~ nom}]}) -%}
|
|
||||||
{%- set x.v = x.v + w -%}
|
|
||||||
{%- endif -%}
|
|
||||||
{%- for p in [
|
|
||||||
{"titre": "Disque utilisé par machine", "seuils": [80, 90],
|
|
||||||
"expr": "100 * (1 - node_filesystem_avail_bytes{job=\"node\",mountpoint=\"/\"} / node_filesystem_size_bytes{job=\"node\",mountpoint=\"/\"})",
|
|
||||||
"desc": "Part utilisée du disque système de chaque machine, de la plus pleine à la moins pleine.\n\n**Seuils** : orange à 80 %, rouge à 90 %.\n\n**Si une barre passe à l'orange** : se connecter à la machine et chercher ce qui grossit (`du -xh / | sort -h | tail`) — le plus souvent des journaux ou un cache ; sinon, agrandir son disque."},
|
|
||||||
{"titre": "Mémoire utilisée par machine", "seuils": [85, 95],
|
|
||||||
"expr": "100 * (1 - node_memory_MemAvailable_bytes{job=\"node\"} / node_memory_MemTotal_bytes{job=\"node\"})",
|
|
||||||
"desc": "Part de la RAM que chaque machine utilise vraiment (cache disque exclu).\n\n**Comment le lire** : une barre très courte sur une machine qui a beaucoup de RAM, c'est de la mémoire qu'on pourrait rendre à l'hyperviseur" ~ (" — et c'est exactement ce qui manque quand le swap des hyperviseurs monte." if fabric else ".") ~ "\n\n**Seuils** : orange à 85 %, rouge à 95 %.\n\n**Si une barre passe au rouge** : la machine manque de mémoire ; lui en donner un peu plus, ou alléger ce qu'elle fait tourner."}
|
|
||||||
] -%}
|
|
||||||
{%- set y.id = y.id + 1 -%}
|
|
||||||
{%- set _ = panneaux.append({
|
|
||||||
"type": "bargauge", "id": y.id, "title": p.titre, "description": p.desc, "datasource": ds,
|
|
||||||
"gridPos": {"h": 9, "w": w, "x": x.v, "y": y.v},
|
|
||||||
"fieldConfig": {"defaults": {"unit": "percent", "min": 0, "max": 100, "decimals": 0,
|
|
||||||
"thresholds": {"mode": "absolute", "steps": [{"color": "green", "value": none}, {"color": "orange", "value": p.seuils[0]}, {"color": "red", "value": p.seuils[1]}]}},
|
|
||||||
"overrides": []},
|
|
||||||
"options": {"displayMode": "gradient", "orientation": "horizontal", "showUnfilled": true, "valueMode": "color",
|
|
||||||
"reduceOptions": {"calcs": ["lastNotNull"], "fields": "", "values": false}},
|
|
||||||
"targets": [{"refId": "A", "datasource": ds, "legendFormat": "{{nodename}}",
|
|
||||||
"expr": "sort_desc(" ~ p.expr ~ nom ~ ")"}]}) -%}
|
|
||||||
{%- set x.v = x.v + w -%}
|
|
||||||
{%- endfor -%}
|
|
||||||
{%- set y.v = y.v + 9 -%}
|
|
||||||
|
|
||||||
{#- ---- RANGEE 3 : QUE SE PASSE-T-IL ? ---------------------------------------------- #}
|
|
||||||
{%- set y.id = y.id + 1 -%}
|
|
||||||
{%- set _ = panneaux.append({"type": "row", "id": y.id, "title": "Que se passe-t-il ?", "collapsed": false,
|
|
||||||
"gridPos": {"h": 1, "w": 24, "x": 0, "y": y.v}}) -%}
|
|
||||||
{%- set y.v = y.v + 1 -%}
|
|
||||||
{%- set job = 'job="fabric"' if fabric else 'job="node"' -%}
|
|
||||||
{%- set qui = "hyperviseur" if fabric else "machine" -%}
|
|
||||||
{%- set y.id = y.id + 1 -%}
|
|
||||||
{%- set _ = panneaux.append({
|
|
||||||
"type": "timeseries", "id": y.id, "title": "Processeur par " ~ qui, "datasource": ds,
|
|
||||||
"description": "Part du processeur occupée, par " ~ qui ~ ".\n\n**Comment le lire** : une courbe basse et plate est normale" ~ (" — 72 cœurs pour la flotte, occupés à quelques pour cent" if fabric else "") ~ ". Les pics racontent ce qui se passe : une reconstruction, une sauvegarde, un déploiement. Une courbe qui reste haute sans raison mérite qu'on cherche quelle machine consomme.\n\n**Si une courbe reste au-dessus de 80 %** : ouvrir le tableau « client metrique » pour voir quelle machine en est la cause.",
|
|
||||||
"gridPos": {"h": 8, "w": 12, "x": 0, "y": y.v},
|
|
||||||
"fieldConfig": {"defaults": {"unit": "percent", "min": 0, "decimals": 0, "color": {"mode": "palette-classic"},
|
|
||||||
"custom": {"drawStyle": "line", "lineWidth": 1, "fillOpacity": 10, "showPoints": "never", "spanNulls": true}}, "overrides": []},
|
|
||||||
"options": {"legend": {"displayMode": "table", "placement": "right", "calcs": ["mean", "max"]}, "tooltip": {"mode": "multi", "sort": "desc"}},
|
|
||||||
"targets": [{"refId": "A", "datasource": ds, "legendFormat": "{{nodename}}",
|
|
||||||
"expr": "100 * (1 - avg by (instance) (rate(node_cpu_seconds_total{mode=\"idle\"," ~ job ~ "}[5m])))" ~ nom}]}) -%}
|
|
||||||
{%- set y.id = y.id + 1 -%}
|
|
||||||
{%- set _ = panneaux.append({
|
|
||||||
"type": "timeseries", "id": y.id, "title": "Trafic réseau par " ~ qui, "datasource": ds,
|
|
||||||
"description": "Débit entrant et sortant additionnés, par " ~ qui ~ " (interfaces physiques seulement).\n\n**Comment le lire** : " ~ ("le trafic courant est surtout celui de Ceph et des sauvegardes ; les grands pics marquent une reconstruction ou un déplacement de VM." if fabric else "les pics marquent les sauvegardes de la nuit, les mises à jour et les échanges entre services.") ~ "\n\n**Si un pic dure sans explication** : croiser avec le tableau « Journaux de la flotte » à la même heure.",
|
|
||||||
"gridPos": {"h": 8, "w": 12, "x": 12, "y": y.v},
|
|
||||||
"fieldConfig": {"defaults": {"unit": "Bps", "min": 0, "decimals": 1, "color": {"mode": "palette-classic"},
|
|
||||||
"custom": {"drawStyle": "line", "lineWidth": 1, "fillOpacity": 10, "showPoints": "never", "spanNulls": true}}, "overrides": []},
|
|
||||||
"options": {"legend": {"displayMode": "table", "placement": "right", "calcs": ["mean", "max"]}, "tooltip": {"mode": "multi", "sort": "desc"}},
|
|
||||||
"targets": [{"refId": "A", "datasource": ds, "legendFormat": "{{nodename}}",
|
|
||||||
"expr": "sum by (instance) (rate(node_network_receive_bytes_total{" ~ job ~ ",device=~\"(en|eth).*\"}[5m]) + rate(node_network_transmit_bytes_total{" ~ job ~ ",device=~\"(en|eth).*\"}[5m]))" ~ nom}]}) -%}
|
|
||||||
{%- set y.v = y.v + 8 -%}
|
|
||||||
|
|
||||||
{#- ---- RANGEE 4 : OU REGARDER ENSUITE ? -------------------------------------------- #}
|
|
||||||
{%- set y.id = y.id + 1 -%}
|
|
||||||
{%- set _ = panneaux.append({"type": "row", "id": y.id, "title": "Où regarder ensuite ?", "collapsed": false,
|
|
||||||
"gridPos": {"h": 1, "w": 24, "x": 0, "y": y.v}}) -%}
|
|
||||||
{%- set y.v = y.v + 1 -%}
|
|
||||||
{%- set liens = [] -%}
|
|
||||||
{%- if fabric -%}{%- set _ = liens.append("- [**Matériel**](/d/setops-materiel) — températures, ventilateurs, disques des hyperviseurs et de la frontière.") -%}{%- endif -%}
|
|
||||||
{%- set _ = liens.append("- [**Machines**](/d/setops-client-metrique) — processeur, mémoire et disque de chaque machine, en détail.") -%}
|
|
||||||
{%- set _ = liens.append("- [**PostgreSQL**](/d/setops-postgresql) — bases de données : taille, connexions, requêtes.") -%}
|
|
||||||
{%- set _ = liens.append("- [**Journaux de la flotte**](/d/setops-journaux) — ce que les machines ont écrit, et quand.") -%}
|
|
||||||
{%- set _ = liens.append("- [**Vigie**](https://vigie." ~ domaine_interne ~ ") — les verdicts d'Icinga : ce qui est cassé, service par service.") -%}
|
|
||||||
{%- set y.id = y.id + 1 -%}
|
|
||||||
{%- set _ = panneaux.append({
|
|
||||||
"type": "text", "id": y.id, "title": "",
|
|
||||||
"gridPos": {"h": 5, "w": 24, "x": 0, "y": y.v},
|
|
||||||
"options": {"mode": "markdown", "content": liens | join("\n")}}) -%}
|
|
||||||
|
|
||||||
{{ {
|
|
||||||
"uid": "setops-synthese",
|
|
||||||
"title": "Set-OPS — Synthèse",
|
|
||||||
"description": "La porte d'entrée de l'observatoire : faut-il agir, reste-t-il de la marge, que se passe-t-il, où regarder ensuite.",
|
|
||||||
"tags": ["set-ops", "derive", "synthese"],
|
|
||||||
"timezone": "browser",
|
|
||||||
"refresh": "1m",
|
|
||||||
"time": {"from": "now-7d", "to": "now"},
|
|
||||||
"schemaVersion": 39,
|
|
||||||
"version": 1,
|
|
||||||
"graphTooltip": 1,
|
|
||||||
"templating": {"list": [
|
|
||||||
{"name": "ds_prom", "label": "Source Prometheus", "type": "datasource", "query": "prometheus", "hide": 2, "current": {}}
|
|
||||||
]},
|
|
||||||
"panels": panneaux
|
|
||||||
} | tojson }}
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
# Supervision derivee du role. Voir docs/supervision-conception.md.
|
# Supervision derivee du role. Voir docs/supervision-conception.md.
|
||||||
sondes:
|
sondes:
|
||||||
- nom: moteur
|
- nom: moteur
|
||||||
metier: supervision
|
|
||||||
ttl: 5400
|
ttl: 5400
|
||||||
raison: 'L''etat des controles arrive-t-il encore jusqu''a la base ? On ne mesure ni le service ni
|
raison: 'L''etat des controles arrive-t-il encore jusqu''a la base ? On ne mesure ni le service ni
|
||||||
le port : on mesure la FRAICHEUR de ce que la base contient. Un moteur vert dont la synchronisation
|
le port : on mesure la FRAICHEUR de ce que la base contient. Un moteur vert dont la synchronisation
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue