supervision : c'est le DEPOT qui dit ou en sont les sauvegardes
Icinga ne surveillait rien : aucun objet Host ni Service de Set-OPS, seulement
la config Debian d'origine sur localhost.
Superviser setops-sauvegarde.service aurait reproduit le defaut du jour meme :
l'unite etait VERTE sur onze noeuds pendant qu'elle n'emportait rien. Le noeud
sait qu'il a LANCE sa sauvegarde, pas qu'elle est ARRIVEE. backup-01 evalue donc
ses depots et pousse un resultat passif par noeud vers l'API Icinga.
Trois criteres, parce qu'un seul suffit a mentir : l'instantane existe, il est
recent (26 h / 50 h), il contient au moins un fichier.
Le sens du flux est delibere : le depot parle a la supervision, jamais l'inverse
— compromettre mon-01 ne donne aucun acces aux sauvegardes.
Le ttl de 6 h fait la fraicheur : si le rapporteur se tait, Icinga perime les
services tout seul. C'est le silence qui a laisse le defaut vivre un mois.
Deux erreurs corrigees par la mesure :
- --data-urlencode refuse en Bad Request (l'API veut du JSON) ; le flux, le TLS
et l'auth marchaient, seule la charge etait perdue.
- le seuil « vide » en octets signalait a tort idm-01 (2363 o) : un export LDIF
d'un annuaire a un compte pese cela. « Vide » se mesure en FICHIERS. Et le
verdict est un AVERTISSEMENT : la machine ne distingue pas « les donnees ont
disparu » de « il n'y en a pas encore ».
Reserve assumee : curl -k — l'API presente le cert de sa propre AC, pas step-ca.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
8eedeaf31f
commit
f1a7e43645
17 changed files with 476 additions and 5 deletions
56
CHANGELOG.md
56
CHANGELOG.md
|
|
@ -1,5 +1,61 @@
|
||||||
# CHANGELOG — Set-OPS
|
# CHANGELOG — Set-OPS
|
||||||
|
|
||||||
|
## 2026-08-11 — Les sauvegardes sont surveillées, et c'est le dépôt qui parle
|
||||||
|
|
||||||
|
Icinga ne surveillait **rien** : aucun objet `Host` ni `Service` de Set-OPS, seulement la
|
||||||
|
configuration Debian d'origine pointant sur `localhost`.
|
||||||
|
|
||||||
|
### On ne supervise pas l'unité — on supervise ce qui est arrivé
|
||||||
|
|
||||||
|
Superviser `setops-sauvegarde.service` aurait reproduit le défaut du jour même : l'unité
|
||||||
|
était **verte** sur onze nœuds pendant qu'elle n'emportait rien. Le nœud sait qu'il a
|
||||||
|
*lancé* sa sauvegarde ; il ne sait pas qu'elle est *arrivée*. Seul le dépôt le voit.
|
||||||
|
|
||||||
|
`backup-01` évalue donc ses dépôts restic et **pousse un résultat passif par nœud** vers
|
||||||
|
l'API Icinga. Trois critères, parce qu'un seul suffit à mentir :
|
||||||
|
|
||||||
|
| Critère | Ce qu'il attrape |
|
||||||
|
|---|---|
|
||||||
|
| l'instantané **existe** | la sauvegarde n'arrive pas |
|
||||||
|
| il est **récent** (26 h / 50 h) | elle a cessé d'arriver |
|
||||||
|
| il contient **au moins un fichier** | elle arrive mais ne porte rien |
|
||||||
|
|
||||||
|
Le sens du flux est délibéré : le dépôt parle à la supervision, jamais l'inverse. Un seul
|
||||||
|
flux nouveau, et compromettre `mon-01` ne donne aucun accès aux sauvegardes.
|
||||||
|
|
||||||
|
### Le silence alerte
|
||||||
|
|
||||||
|
Le `ttl` de 6 h porté par chaque envoi fait la fraîcheur : si le rapporteur se tait,
|
||||||
|
Icinga périme les services tout seul. **C'est le silence qui a laissé le défaut vivre un
|
||||||
|
mois** — il devait devenir la première chose qui alerte. Le rapporteur, lui, refuse
|
||||||
|
d'avaler ses propres échecs et sort en erreur.
|
||||||
|
|
||||||
|
### Deux erreurs de conception, corrigées par la mesure
|
||||||
|
|
||||||
|
**Le corps `--data-urlencode` était refusé** en `Bad Request` : l'API veut du JSON. Le flux,
|
||||||
|
le TLS et l'authentification fonctionnaient — seule la charge était perdue. Sans lecture du
|
||||||
|
journal d'Icinga, un `curl` silencieux aurait été pris pour un succès.
|
||||||
|
|
||||||
|
**Le seuil « vide » en octets était faux.** Il signalait `idm-01` (2 363 octets) alors qu'un
|
||||||
|
export LDIF d'un annuaire à un compte pèse légitimement cela. « Vide » se mesure en
|
||||||
|
**fichiers**, pas en taille : zéro fichier, c'est exact quelle que soit la taille. Et le
|
||||||
|
verdict est un **avertissement**, pas un critique — la machine ne peut pas distinguer « les
|
||||||
|
données ont disparu » de « il n'y en a pas encore », mais l'humain doit le voir.
|
||||||
|
|
||||||
|
### Mesuré de bout en bout
|
||||||
|
|
||||||
|
```
|
||||||
|
idm-01 OK 1 fichier, 2 363 o (l'annuaire) infra-mail-01 AVERT. aucun fichier
|
||||||
|
infra-pki-01 OK 20 621 o (les clés de l'AC) web-frontal-01 AVERT. aucun fichier
|
||||||
|
collab-01 OK 67 129 221 o web-dorsal-01 AVERT. aucun fichier
|
||||||
|
data-sql-01 OK 1 085 158 o (toutes les bases)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Réserve assumée** : le rapporteur appelle l'API en `curl -k`. L'API Icinga présente le
|
||||||
|
certificat de sa propre AC (`icinga2 api setup`), pas celui de step-ca — la liaison est
|
||||||
|
chiffrée mais le pair n'est pas vérifié. C'est la réserve connue sur `5665`, et elle reste
|
||||||
|
ouverte.
|
||||||
|
|
||||||
## 2026-08-11 — La sauvegarde emporte enfin quelque chose
|
## 2026-08-11 — La sauvegarde emporte enfin quelque chose
|
||||||
|
|
||||||
**Correction de l'entrée précédente** : j'y attribuais le défaut à la reconstruction
|
**Correction de l'entrée précédente** : j'y attribuais le défaut à la reconstruction
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@
|
||||||
| P06 | Validateurs de registres (serveurs/apps/bases/domaines) | AFF-003 | ✅ OK | Registre des domaines valide. |
|
| 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). |
|
| P07 | GUI (node --check) | AFF-033 | ✅ OK | JS du GUI : syntaxe valide (node --check). |
|
||||||
| P08 | Orchestration (couches + graphe) | AFF-070 | ✅ OK | Orchestration coherente : 30 groupes classes, aucun cycle, aucune arete en arriere. |
|
| P08 | Orchestration (couches + graphe) | AFF-070 | ✅ OK | Orchestration coherente : 30 groupes classes, aucun cycle, aucune arete en arriere. |
|
||||||
| P09 | Flux reseau (schema + matrice) | AFF-071 | ✅ OK | Flux coherents : 29 rôles, 75 flux, schéma + matrice OK. |
|
| P09 | Flux reseau (schema + matrice) | AFF-071 | ✅ OK | Flux coherents : 29 rôles, 77 flux, schéma + matrice OK. |
|
||||||
| P10 | Handlers <-> notify | AFF-034, AFF-035 | ✅ OK | Tout notify pointe vers un handler du meme role (49 roles). |
|
| 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 | playbook: playbooks/proxmox/cloner_vm_debian.yml |
|
| P11 | Syntaxe des playbooks (--syntax-check) | AFF-083 | ✅ OK | playbook: playbooks/proxmox/cloner_vm_debian.yml |
|
||||||
| P12 | Existence des runbooks cites | AFF-010, AFF-011, AFF-012, AFF-083 | ✅ OK | 17/17 runbooks/registres cites presents. |
|
| P12 | Existence des runbooks cites | AFF-010, AFF-011, AFF-012, AFF-083 | ✅ OK | 17/17 runbooks/registres cites presents. |
|
||||||
|
|
@ -30,21 +30,21 @@
|
||||||
| P15 | Modele public socle valide | AFF-022, AFF-099 | ✅ OK | Modele public socle : domaines/serveurs/applications/bases valides. |
|
| 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 | 14 hotes, 31 groupes (inventaire dechiffre et parse). |
|
| P16 | Inventaire Ansible complet (--list) | AFF-030 | ✅ OK | 14 hotes, 31 groupes (inventaire dechiffre et parse). |
|
||||||
| P17 | Tous les modeles valident (registres + underlay) | AFF-022, AFF-099 | ✅ OK | Les 1 modele(s) decouvert(s) valident. |
|
| 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 : 25 secret(s) exige(s), tous presents. Voute reelle : 28 cle(s), aucun manque. |
|
| P18 | Gabarit de voute complet | AFF-026 | ✅ OK | Gabarit de voute complet : 26 secret(s) exige(s), tous presents. Voute reelle : 29 cle(s), aucun manque. |
|
||||||
| P19 | Le GUI couvre le schema du plan | AFF-002, AFF-095 | ✅ OK | GUI : les 28 champ(s) des plans reels sont editables (2 plan(s) inspecte(s)), registres toleres : nomenclature. |
|
| P19 | Le GUI couvre le schema du plan | AFF-002, AFF-095 | ✅ OK | GUI : les 28 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) : adressage 100% derive du seed index. |
|
| P20 | Adressage 100% derive du seed (aucun stocke) | AFF-001, AFF-003 | ✅ OK | 2 nomenclature(s) : adressage 100% derive du seed index. |
|
||||||
| P21 | Federation : aucun index en collision | AFF-102 | ✅ OK | Federation coherente : 2 instance(s) federee(s), aucun index en collision. |
|
| P21 | Federation : aucun index en collision | AFF-102 | ✅ OK | Federation coherente : 2 instance(s) federee(s), aucun index en collision. |
|
||||||
| P22 | Plan de recette a jour (genere du wiki) | AFF-002 | ✅ OK | Plan de recette à jour (20 sections). |
|
| P22 | Plan de recette a jour (genere du wiki) | AFF-002 | ✅ OK | Plan de recette à jour (20 sections). |
|
||||||
| P23 | Underlay sans collision avec la plage tenant | AFF-103 | ✅ OK | Underlay conforme : 6 reseau(x), aucune collision avec la plage tenant. |
|
| P23 | Underlay sans collision avec la plage tenant | AFF-103 | ✅ OK | Underlay conforme : 6 reseau(x), aucune collision avec la plage tenant. |
|
||||||
| P24 | Frontiere nord/sud : acces d'administration declare | AFF-104 | ✅ OK | CONFORME : frontiere nord/sud, 39 regles, 12 routes, admin=10.0.0.0/24,192.168.254.2/32,192.168.255.2/32. |
|
| P24 | Frontiere nord/sud : acces d'administration declare | AFF-104 | ✅ OK | CONFORME : frontiere nord/sud, 39 regles, 12 routes, admin=10.0.0.0/24,192.168.254.2/32,192.168.255.2/32. |
|
||||||
| P25 | Pare-feu Proxmox : est-ouest intra-tenant derive | AFF-107 | ✅ OK | CONFORME : pare-feu Proxmox, 2 tenant(s), 38 groupe(s), 64 regle(s). |
|
| P25 | Pare-feu Proxmox : est-ouest intra-tenant derive | AFF-107 | ✅ OK | CONFORME : pare-feu Proxmox, 2 tenant(s), 40 groupe(s), 66 regle(s). |
|
||||||
| P26 | Integrations universelles : aucun hote laisse de cote | AFF-108 | ✅ OK | 14 hote(s) x 4 integration(s) universelle(s) : aucune lacune, aucune recopie (1 exemption(s) derivee(s) du service rendu). |
|
| P26 | Integrations universelles : aucun hote laisse de cote | AFF-108 | ✅ OK | 14 hote(s) x 4 integration(s) universelle(s) : aucune lacune, aucune recopie (1 exemption(s) derivee(s) du service rendu). |
|
||||||
| P27 | Propriete des intrants : hebergeur et tenant separes | AFF-109 | ✅ OK | 0 cle(s) de cluster chez l'hebergeur, aucune recopiee dans les group_vars du tenant. |
|
| P27 | Propriete des intrants : hebergeur et tenant separes | AFF-109 | ✅ OK | 0 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 : 2 pool(s) Proxmox, 28 VM placee(s), aucun nom ni VMID en collision. |
|
| P28 | Pools Proxmox : un par tenant, sans collision | AFF-110 | ✅ OK | CONFORME : 2 pool(s) Proxmox, 28 VM placee(s), aucun nom ni VMID en collision. |
|
||||||
| P29 | Authentification : chaque role declare sa position | AFF-111 | ✅ OK | 23 role(s) serveur declares (interne-sans-auth 2, ldap-direct 2, sans-auth-humaine 12, socle-identite 2, web-sso 5) ; 2 lacune(s) nommee(s) : serveur_loki, serv |
|
| P29 | Authentification : chaque role declare sa position | AFF-111 | ✅ OK | 23 role(s) serveur declares (interne-sans-auth 2, ldap-direct 2, sans-auth-humaine 12, 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. |
|
| 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 | 44 scripts expliques et atteignables, 92 cibles make documentees, 54 roles avec README. |
|
| P31 | Documentation : tout ce que le depot FAIT est nomme | — | ✅ OK | 44 scripts expliques et atteignables, 92 cibles make documentees, 54 roles avec README. |
|
||||||
| P32 | Intrants exiges par les roles : tous fournis | — | ✅ OK | CONFORME : 30 exigence(s) de role, toutes satisfaites (126 cle(s) declaree(s) par l'instance). |
|
| P32 | Intrants exiges par les roles : tous fournis | — | ✅ OK | CONFORME : 35 exigence(s) de role, toutes satisfaites (126 cle(s) declaree(s) par l'instance). |
|
||||||
| P33 | Aucune collision de port entre roles co-localises | — | ✅ OK | CONFORME : 32 revendication(s) de port, aucune collision entre roles co-localises (33 groupes). |
|
| P33 | Aucune collision de port entre roles co-localises | — | ✅ OK | CONFORME : 32 revendication(s) de port, aucune collision entre roles co-localises (33 groupes). |
|
||||||
| P34 | Chaque document declare son lecteur | — | ✅ OK | 38 document(s) declarent leur lecteur (14 genere(s) exempte(s)). |
|
| P34 | Chaque document declare son lecteur | — | ✅ OK | 38 document(s) declarent leur lecteur (14 genere(s) exempte(s)). |
|
||||||
| P35 | Toute application exigeant une base en a une au plan | — | ✅ OK | 5 application(s) exigeant une base l'ont toutes (4 entree(s) au registre). |
|
| P35 | Toute application exigeant une base en a une au plan | — | ✅ OK | 5 application(s) exigeant une base l'ont toutes (4 entree(s) au registre). |
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
| `client_unbound` | egress | 53 | udp | externe | clair | Récursion DNS depuis la racine (UDP d'abord), validée par DNSSEC — la confidentialité du transport n'est pas l'enjeu, l'authenticité l'est. |
|
| `client_unbound` | egress | 53 | udp | externe | clair | Récursion DNS depuis la racine (UDP d'abord), validée par DNSSEC — la confidentialité du transport n'est pas l'enjeu, l'authenticité l'est. |
|
||||||
| `client_unbound` | egress | 53 | tcp | externe | clair | Récursion DNS en TCP : repli obligatoire quand la réponse dépasse la taille UDP (fréquent avec DNSSEC). |
|
| `client_unbound` | egress | 53 | tcp | externe | clair | Récursion DNS en TCP : repli obligatoire quand la réponse dépasse la taille UDP (fréquent avec DNSSEC). |
|
||||||
| `serveur_backup` | ingress | 22 | tcp | client_backup | ssh | Dépôt restic servi par SSH (utilisateur restreint restic + clé) ; chaque client_backup pousse ses instantanés. |
|
| `serveur_backup` | ingress | 22 | tcp | client_backup | ssh | Dépôt restic servi par SSH (utilisateur restreint restic + clé) ; chaque client_backup pousse ses instantanés. |
|
||||||
|
| `serveur_backup` | egress | 5665 | tcp | serveur_icinga | tls-requis | Rapport passif des sauvegardes vers l'API Icinga : le depot est le seul a voir ce qui est reellement arrive. |
|
||||||
| `serveur_collabora` | ingress | 9980 | tcp | edge | clair | Éditeur servi au navigateur via l'edge (WebSocket WOPI ; TLS terminé à l'edge). |
|
| `serveur_collabora` | ingress | 9980 | tcp | edge | clair | Éditeur servi au navigateur via l'edge (WebSocket WOPI ; TLS terminé à l'edge). |
|
||||||
| `serveur_collabora` | ingress | 9980 | tcp | localhost | clair | Vérifications WOPI serveur→Collabora depuis Nextcloud co-localisé. |
|
| `serveur_collabora` | ingress | 9980 | tcp | localhost | clair | Vérifications WOPI serveur→Collabora depuis Nextcloud co-localisé. |
|
||||||
| `serveur_debian` | ingress | 22 | tcp | flotte, externe | ssh | Plan de gestion : administration et déploiement Ansible par SSH (inter-nœud ; l'accès depuis l'extérieur est filtré à l'OPNsense). |
|
| `serveur_debian` | ingress | 22 | tcp | flotte, externe | ssh | Plan de gestion : administration et déploiement Ansible par SSH (inter-nœud ; l'accès depuis l'extérieur est filtré à l'OPNsense). |
|
||||||
|
|
@ -36,6 +37,7 @@
|
||||||
| `serveur_grafana` | ingress | 3000 | tcp | edge | clair | Interface web servie via l'edge (TLS terminé à l'edge). |
|
| `serveur_grafana` | ingress | 3000 | tcp | edge | clair | Interface web servie via l'edge (TLS terminé à l'edge). |
|
||||||
| `serveur_grafana` | egress | 443 | tcp | edge | tls-requis | Authentification OIDC auprès de Keycloak (via son FQDN publié à l'edge). |
|
| `serveur_grafana` | egress | 443 | tcp | edge | tls-requis | Authentification OIDC auprès de Keycloak (via son FQDN publié à l'edge). |
|
||||||
| `serveur_icinga` | ingress | 5665 | tcp | localhost | clair | API Icinga 2 consommée en local par Icinga Web 2 co-localisé. |
|
| `serveur_icinga` | ingress | 5665 | tcp | localhost | clair | API Icinga 2 consommée en local par Icinga Web 2 co-localisé. |
|
||||||
|
| `serveur_icinga` | ingress | 5665 | tcp | serveur_backup | tls-requis | Le depot de sauvegarde depose ses resultats passifs (portee : process-check-result sur « sauvegarde: * »). |
|
||||||
| `serveur_icinga` | egress | 5432 | tcp | serveur_postgresql | tls-requis | Base relationnelle du moteur Icinga (verify-full). |
|
| `serveur_icinga` | egress | 5432 | tcp | serveur_postgresql | tls-requis | Base relationnelle du moteur Icinga (verify-full). |
|
||||||
| `serveur_icingaweb2` | ingress | 8080 | tcp | edge | clair | Interface web servie via l'edge (TLS terminé à l'edge ; SSO possible via oauth2-proxy). |
|
| `serveur_icingaweb2` | ingress | 8080 | tcp | edge | clair | Interface web servie via l'edge (TLS terminé à l'edge ; SSO possible via oauth2-proxy). |
|
||||||
| `serveur_icingaweb2` | egress | 636 | tcp | serveur_openldap | tls-requis | Authentification des utilisateurs sur l'annuaire (LDAPS). |
|
| `serveur_icingaweb2` | egress | 636 | tcp | serveur_openldap | tls-requis | Authentification des utilisateurs sur l'annuaire (LDAPS). |
|
||||||
|
|
@ -90,4 +92,4 @@
|
||||||
- **starttls** : 6 flux
|
- **starttls** : 6 flux
|
||||||
- **tls** : 8 flux
|
- **tls** : 8 flux
|
||||||
- **tls-cible** : 2 flux
|
- **tls-cible** : 2 flux
|
||||||
- **tls-requis** : 24 flux
|
- **tls-requis** : 26 flux
|
||||||
|
|
|
||||||
|
|
@ -86,3 +86,6 @@ client_backup_catalogue:
|
||||||
client_backup_jobs: >-
|
client_backup_jobs: >-
|
||||||
{{ client_backup_catalogue | dict2items
|
{{ client_backup_catalogue | dict2items
|
||||||
| selectattr('key', 'in', group_names) | map(attribute='value') | list }}
|
| selectattr('key', 'in', group_names) | map(attribute='value') | list }}
|
||||||
|
|
||||||
|
# La liste EN CLAIR des memes groupes vit dans `vars/main.yml` — sans Jinja, pour que la
|
||||||
|
# supervision et P36 puissent la lire par `include_vars` sans rendre ce catalogue.
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,17 @@
|
||||||
# On ne refuse donc pas le deploiement : on refuse d'installer une sauvegarde vide, et on
|
# On ne refuse donc pas le deploiement : on refuse d'installer une sauvegarde vide, et on
|
||||||
# RETIRE celle qui existerait. Que tout detenteur d'etat porte bien `client_backup` est
|
# RETIRE celle qui existerait. Que tout detenteur d'etat porte bien `client_backup` est
|
||||||
# lisible dans le plan, donc prouve statiquement (D-75, P36) — pas ici.
|
# lisible dans le plan, donc prouve statiquement (D-75, P36) — pas ici.
|
||||||
|
- name: Refuser une porte d'entree du catalogue devenue fausse
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- (client_backup_catalogue.keys() | list | sort) == (client_backup_groupes_etat | sort)
|
||||||
|
fail_msg: >-
|
||||||
|
`client_backup_groupes_etat` ne decrit plus `client_backup_catalogue` :
|
||||||
|
catalogue={{ client_backup_catalogue.keys() | list | sort }},
|
||||||
|
liste={{ client_backup_groupes_etat | sort }}. La supervision et P36 lisent la
|
||||||
|
liste — la laisser diverger, c'est sauvegarder sans surveiller, ou surveiller ce
|
||||||
|
qui n'existe pas.
|
||||||
|
|
||||||
- name: Etat de la sauvegarde pour ce noeud
|
- name: Etat de la sauvegarde pour ce noeud
|
||||||
ansible.builtin.debug:
|
ansible.builtin.debug:
|
||||||
msg: >-
|
msg: >-
|
||||||
|
|
|
||||||
23
roles/client_backup/vars/main.yml
Normal file
23
roles/client_backup/vars/main.yml
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
---
|
||||||
|
# Groupes DETENTEURS D'ETAT — la porte d'entree publique du catalogue de sauvegarde.
|
||||||
|
#
|
||||||
|
# Pourquoi un fichier a part, et SANS Jinja. Deux consommateurs externes ont besoin de
|
||||||
|
# cette liste : `serveur_backup` (qui rapporte a Icinga l'etat reel des instantanes) et
|
||||||
|
# `serveur_icinga` (qui cree les objets a surveiller). Tous deux la lisent par
|
||||||
|
# `include_vars` — et `include_vars` charge le FICHIER ENTIER. Charger `defaults/main.yml`
|
||||||
|
# forcerait le rendu de `client_backup_repo`, qui reference des variables absentes de leur
|
||||||
|
# play : le deploiement echouait sur « client_backup_utilisateur_distant is undefined »
|
||||||
|
# (mesure le 2026-08-11). Ce fichier ne contient donc que des litteraux.
|
||||||
|
#
|
||||||
|
# `tasks/main.yml` REFUSE si cette liste et `client_backup_catalogue` divergent : on ne
|
||||||
|
# peut pas ajouter un detenteur d'etat sans que la supervision et P36 l'apprennent.
|
||||||
|
client_backup_groupes_etat:
|
||||||
|
- serveur_step_ca
|
||||||
|
- serveur_openldap
|
||||||
|
- serveur_postgresql
|
||||||
|
- serveur_dovecot
|
||||||
|
- serveur_forgejo
|
||||||
|
- serveur_nextcloud
|
||||||
|
- serveur_rspamd
|
||||||
|
- serveur_web_frontal
|
||||||
|
- serveur_web_dorsal
|
||||||
|
|
@ -5,3 +5,34 @@ serveur_backup_utilisateur: "restic"
|
||||||
serveur_backup_racine: "/srv/restic"
|
serveur_backup_racine: "/srv/restic"
|
||||||
# Clé PUBLIQUE de sauvegarde autorisée (la privée est dans la voûte, côté client_backup).
|
# Clé PUBLIQUE de sauvegarde autorisée (la privée est dans la voûte, côté client_backup).
|
||||||
serveur_backup_pubkey: ""
|
serveur_backup_pubkey: ""
|
||||||
|
|
||||||
|
# --- Vérification des dépôts et rapport passif vers Icinga ---
|
||||||
|
# Le dépôt est le SEUL à voir ce qui est réellement arrivé. Un nœud sait qu'il a lancé sa
|
||||||
|
# sauvegarde ; il ne sait pas qu'elle a abouti. D'où la vérification ici, et pas là-bas.
|
||||||
|
# Toutes les 4 h, pas une fois par jour : la vérification doit renouveler le `ttl` bien
|
||||||
|
# avant qu'il n'expire, sinon un simple retard du timer se lirait comme une sauvegarde
|
||||||
|
# perdue. Elle est peu coûteuse (lecture des métadonnées restic).
|
||||||
|
serveur_backup_verification_horaire: "*-*-* 00/4:00:00"
|
||||||
|
|
||||||
|
# Seuils d'âge. Le critère « n'emporte rien » ne se règle pas ici : il se mesure en
|
||||||
|
# NOMBRE DE FICHIERS dans l'instantané (zéro = rien emporté), ce qui est exact, alors
|
||||||
|
# qu'un seuil en octets signalait à tort un export LDIF de 2,3 Ko.
|
||||||
|
serveur_backup_age_warn_h: 26
|
||||||
|
serveur_backup_age_crit_h: 50
|
||||||
|
|
||||||
|
# `ttl` du résultat passif : au-delà, Icinga périme le service de lui-même — c'est ce qui
|
||||||
|
# fait que le SILENCE alerte. 6 h pour une vérification toutes les 4 h : une exécution peut
|
||||||
|
# être manquée sans fausse alerte, deux ne le peuvent pas.
|
||||||
|
serveur_backup_ttl_icinga: 21600
|
||||||
|
|
||||||
|
# Compte d'API Icinga (portée : uniquement process-check-result sur « sauvegarde: * »).
|
||||||
|
serveur_backup_icinga_utilisateur: "setops-depot"
|
||||||
|
serveur_backup_icinga_motdepasse: "{{ vault_icinga_api_depot | default('') }}"
|
||||||
|
serveur_backup_icinga_hote: "{{ (groups['serveur_icinga'] | default([]) | first) | default('') }}"
|
||||||
|
|
||||||
|
# Nœuds dont un instantané est ATTENDU : ceux qui portent client_backup ET détiennent
|
||||||
|
# réellement de l'état. Même règle que `client_backup_jobs`, même source que P36.
|
||||||
|
serveur_backup_noeuds_attendus: >-
|
||||||
|
{{ (client_backup_groupes_etat | default([]) | map('extract', groups)
|
||||||
|
| select('defined') | flatten | unique | list)
|
||||||
|
| intersect(groups['client_backup'] | default([])) }}
|
||||||
|
|
|
||||||
|
|
@ -12,3 +12,9 @@ flux:
|
||||||
# le 22 — alors qu'il n'y a qu'un seul sshd.
|
# le 22 — alors qu'il n'y a qu'un seul sshd.
|
||||||
partage: true
|
partage: true
|
||||||
raison: "Dépôt restic servi par SSH (utilisateur restreint restic + clé) ; chaque client_backup pousse ses instantanés."
|
raison: "Dépôt restic servi par SSH (utilisateur restreint restic + clé) ; chaque client_backup pousse ses instantanés."
|
||||||
|
- sens: egress
|
||||||
|
port: 5665
|
||||||
|
protocole: tcp
|
||||||
|
pair: serveur_icinga
|
||||||
|
chiffrement: tls-requis
|
||||||
|
raison: "Rapport passif des sauvegardes vers l'API Icinga : le depot est le seul a voir ce qui est reellement arrive."
|
||||||
|
|
|
||||||
|
|
@ -26,3 +26,85 @@
|
||||||
user: "{{ serveur_backup_utilisateur }}"
|
user: "{{ serveur_backup_utilisateur }}"
|
||||||
key: "{{ serveur_backup_pubkey }}"
|
key: "{{ serveur_backup_pubkey }}"
|
||||||
state: present
|
state: present
|
||||||
|
|
||||||
|
# --- Vérification des dépôts et rapport passif vers Icinga ---
|
||||||
|
# La liste des détenteurs d'état appartient à `client_backup`. On la LIT chez lui : deux
|
||||||
|
# listes finissent toujours par diverger, et la divergence se lirait « tout va bien ».
|
||||||
|
- name: Lire la liste des détenteurs d'état chez client_backup
|
||||||
|
ansible.builtin.include_vars:
|
||||||
|
file: "{{ role_path }}/../client_backup/vars/main.yml"
|
||||||
|
name: _catalogue_sauvegarde
|
||||||
|
|
||||||
|
- name: Adopter la liste des détenteurs d'état
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
client_backup_groupes_etat: "{{ _catalogue_sauvegarde.client_backup_groupes_etat }}"
|
||||||
|
|
||||||
|
- name: Exiger de quoi rapporter à Icinga (Vault + hôte de supervision)
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- serveur_backup_icinga_motdepasse | length > 0
|
||||||
|
- serveur_backup_icinga_hote | length > 0
|
||||||
|
- serveur_backup_noeuds_attendus | length > 0
|
||||||
|
fail_msg: >-
|
||||||
|
vault_icinga_api_depot requis, un hôte du groupe `serveur_icinga` doit exister, et
|
||||||
|
au moins un nœud doit détenir de l'état. Sans cela, les sauvegardes ne seraient
|
||||||
|
surveillées par personne — c'est exactement le silence qui a laissé le défaut du
|
||||||
|
2026-07-03 vivre un mois.
|
||||||
|
|
||||||
|
- name: Installer curl et le mot de passe restic pour la vérification
|
||||||
|
ansible.builtin.apt:
|
||||||
|
name: [curl, restic]
|
||||||
|
state: present
|
||||||
|
|
||||||
|
- 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 le mot de passe restic (lecture des dépôts)
|
||||||
|
ansible.builtin.copy:
|
||||||
|
content: "{{ vault_restic_password }}\n"
|
||||||
|
dest: /etc/setops/restic.pass
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: "0600"
|
||||||
|
no_log: true
|
||||||
|
|
||||||
|
- name: Déposer le mot de passe d'API Icinga
|
||||||
|
ansible.builtin.copy:
|
||||||
|
content: "{{ serveur_backup_icinga_motdepasse }}"
|
||||||
|
dest: /etc/setops/icinga-api.pass
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: "0600"
|
||||||
|
no_log: true
|
||||||
|
|
||||||
|
- name: Déployer le script de vérification
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: verifier-sauvegardes.sh.j2
|
||||||
|
dest: /usr/local/sbin/setops-verifier-sauvegardes.sh
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: "0700"
|
||||||
|
|
||||||
|
- name: Déployer l'unité et le timer de vérification
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: "{{ item.s }}"
|
||||||
|
dest: "/etc/systemd/system/{{ item.d }}"
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: "0644"
|
||||||
|
loop:
|
||||||
|
- { s: setops-verification.service.j2, d: setops-verification.service }
|
||||||
|
- { s: setops-verification.timer.j2, d: setops-verification.timer }
|
||||||
|
|
||||||
|
- name: Activer le timer de vérification
|
||||||
|
when: not ansible_check_mode
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: setops-verification.timer
|
||||||
|
enabled: true
|
||||||
|
state: started
|
||||||
|
daemon_reload: true
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
# Géré par Set-OPS (rôle serveur_backup). Ne pas éditer à la main.
|
||||||
|
[Unit]
|
||||||
|
Description=Vérification des dépôts restic et rapport passif vers Icinga
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/usr/local/sbin/setops-verifier-sauvegardes.sh
|
||||||
|
Nice=10
|
||||||
|
IOSchedulingClass=idle
|
||||||
14
roles/serveur_backup/templates/setops-verification.timer.j2
Normal file
14
roles/serveur_backup/templates/setops-verification.timer.j2
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
# Géré par Set-OPS (rôle serveur_backup). Ne pas éditer à la main.
|
||||||
|
# Plus frequent que la sauvegarde elle-meme : le `ttl` envoye a Icinga doit etre
|
||||||
|
# renouvele bien avant d'expirer, sinon un simple retard de ce timer se lirait comme
|
||||||
|
# une sauvegarde perdue.
|
||||||
|
[Unit]
|
||||||
|
Description=Planification de la vérification des sauvegardes Set-OPS
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnCalendar={{ serveur_backup_verification_horaire }}
|
||||||
|
RandomizedDelaySec=300
|
||||||
|
Persistent=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
108
roles/serveur_backup/templates/verifier-sauvegardes.sh.j2
Normal file
108
roles/serveur_backup/templates/verifier-sauvegardes.sh.j2
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Gere par Set-OPS (role serveur_backup). Ne pas editer a la main.
|
||||||
|
#
|
||||||
|
# Verifie les depots restic et POUSSE un resultat passif par noeud vers l'API Icinga.
|
||||||
|
#
|
||||||
|
# Pourquoi ici et pas sur le noeud source. Le noeud sait s'il a LANCE sa sauvegarde ; il
|
||||||
|
# ne sait pas si elle est ARRIVEE. Le depot, lui, voit ce qui existe reellement — et c'est
|
||||||
|
# la seule chose qui compte le jour d'une restauration. Le 2026-08-11, onze noeuds
|
||||||
|
# lancaient chaque nuit une sauvegarde qui n'emportait RIEN : une unite verte sur un depot
|
||||||
|
# vide. Trois criteres, donc, et pas un seul :
|
||||||
|
#
|
||||||
|
# 1. l'instantane EXISTE (sinon : la sauvegarde n'arrive pas)
|
||||||
|
# 2. il est RECENT (sinon : elle a cesse d'arriver)
|
||||||
|
# 3. il n'est pas VIDE (sinon : elle arrive mais ne porte rien)
|
||||||
|
#
|
||||||
|
# Le `ttl` envoye a Icinga fait la fraicheur : si ce script cesse de tourner, Icinga passe
|
||||||
|
# tout seul en « expire ». C'est le SILENCE qui alerte — c'est lui qui n'a alerte personne.
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
RACINE="{{ serveur_backup_racine }}"
|
||||||
|
API="https://{{ serveur_backup_icinga_hote }}.{{ domaine_interne }}:5665"
|
||||||
|
TTL={{ serveur_backup_ttl_icinga }}
|
||||||
|
AGE_WARN={{ serveur_backup_age_warn_h }}
|
||||||
|
AGE_CRIT={{ serveur_backup_age_crit_h }}
|
||||||
|
|
||||||
|
export RESTIC_PASSWORD_FILE="/etc/setops/restic.pass"
|
||||||
|
|
||||||
|
# L'API Icinga veut du JSON : un envoi `--data-urlencode` est refuse en « Bad Request »
|
||||||
|
# et le corps est perdu (mesure le 2026-08-11). On construit donc la charge avec python3,
|
||||||
|
# qui echappe correctement le texte du diagnostic.
|
||||||
|
ECHECS=0
|
||||||
|
rapporter() { # $1=noeud $2=code $3=texte
|
||||||
|
local charge reponse
|
||||||
|
charge=$(python3 -c 'import json,sys; print(json.dumps({
|
||||||
|
"type": "Service", "service": sys.argv[1], "exit_status": int(sys.argv[2]),
|
||||||
|
"plugin_output": sys.argv[3], "ttl": int(sys.argv[4])}))' \
|
||||||
|
"${HOTE_DEPOT}!sauvegarde: $1" "$2" "$3" "${TTL}")
|
||||||
|
reponse=$(curl -sS -k --max-time 20 \
|
||||||
|
-u "{{ serveur_backup_icinga_utilisateur }}:${MOTDEPASSE}" \
|
||||||
|
-H 'Accept: application/json' -H 'Content-Type: application/json' \
|
||||||
|
-X POST "${API}/v1/actions/process-check-result" -d "${charge}" 2>&1)
|
||||||
|
# Ne PAS avaler l'echec : un rapporteur muet recreerait exactement le defaut qu'on
|
||||||
|
# corrige. Le `ttl` reste le filet — si ce script cesse d'aboutir, Icinga perime les
|
||||||
|
# services de lui-meme et c'est le silence qui alerte.
|
||||||
|
if ! printf '%s' "${reponse}" | grep -q '"code": *200'; then
|
||||||
|
echo "ECHEC du rapport Icinga pour $1 : ${reponse}" >&2
|
||||||
|
ECHECS=$((ECHECS + 1))
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
MOTDEPASSE="$(cat /etc/setops/icinga-api.pass)"
|
||||||
|
HOTE_DEPOT="{{ inventory_hostname }}"
|
||||||
|
maintenant=$(date +%s)
|
||||||
|
|
||||||
|
for noeud in {{ serveur_backup_noeuds_attendus | sort | join(' ') }}; do
|
||||||
|
depot="${RACINE}/${noeud}"
|
||||||
|
if [[ ! -d "${depot}" ]]; then
|
||||||
|
rapporter "${noeud}" 2 "AUCUN DEPOT : ${depot} n'existe pas — ce noeud n'a jamais depose."
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
json=$(restic -r "${depot}" snapshots --latest 1 --json 2>/dev/null)
|
||||||
|
horodatage=$(printf '%s' "${json}" | python3 -c \
|
||||||
|
'import sys,json;d=json.load(sys.stdin);print(d[0]["time"] if d else "")' 2>/dev/null)
|
||||||
|
if [[ -z "${horodatage}" ]]; then
|
||||||
|
rapporter "${noeud}" 2 "AUCUN INSTANTANE dans ${depot} — depot present mais vide."
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# restic ecrit des fractions de seconde a precision variable : on tronque avant %z.
|
||||||
|
secondes=$(date -d "$(printf '%s' "${horodatage}" | sed -E 's/\.[0-9]+//')" +%s 2>/dev/null || echo 0)
|
||||||
|
age_h=$(( (maintenant - secondes) / 3600 ))
|
||||||
|
|
||||||
|
taille=$(restic -r "${depot}" stats latest --mode raw-data --json 2>/dev/null | python3 -c \
|
||||||
|
'import sys,json;print(json.load(sys.stdin).get("total_size",0))' 2>/dev/null || echo 0)
|
||||||
|
|
||||||
|
# « Vide » se mesure en FICHIERS, pas en octets. Un seuil en octets est un mauvais
|
||||||
|
# critere : l'export LDIF d'un annuaire a un compte pese 2,3 Ko et se ferait signaler a
|
||||||
|
# tort (mesure le 2026-08-11). Un instantane qui ne contient AUCUN fichier — seulement
|
||||||
|
# les repertoires traverses — n'emporte rien, et c'est exact quelle que soit la taille.
|
||||||
|
fichiers=$(restic -r "${depot}" ls latest --json 2>/dev/null | python3 -c \
|
||||||
|
'import sys,json
|
||||||
|
n=0
|
||||||
|
for l in sys.stdin:
|
||||||
|
try: d=json.loads(l)
|
||||||
|
except ValueError: continue
|
||||||
|
if d.get("struct_type")=="node" and d.get("type")=="file": n+=1
|
||||||
|
print(n)' 2>/dev/null || echo 0)
|
||||||
|
|
||||||
|
if (( fichiers == 0 )); then
|
||||||
|
# AVERTISSEMENT et non CRITIQUE : la machine ne peut pas distinguer « les donnees ont
|
||||||
|
# disparu » de « il n'y en a pas encore » (un /var/vmail sans courriel, un /srv/web
|
||||||
|
# sans webapp). C'est a un humain de trancher — mais il doit le VOIR.
|
||||||
|
rapporter "${noeud}" 1 \
|
||||||
|
"N'EMPORTE RIEN : instantane sans aucun fichier (${taille} octets). Legitime si ce noeud n'a pas encore de donnees — a confirmer."
|
||||||
|
elif (( age_h >= AGE_CRIT )); then
|
||||||
|
rapporter "${noeud}" 2 "PERIME : dernier instantane il y a ${age_h} h (seuil ${AGE_CRIT} h), ${fichiers} fichier(s)."
|
||||||
|
elif (( age_h >= AGE_WARN )); then
|
||||||
|
rapporter "${noeud}" 1 "EN RETARD : dernier instantane il y a ${age_h} h (seuil ${AGE_WARN} h), ${fichiers} fichier(s)."
|
||||||
|
else
|
||||||
|
rapporter "${noeud}" 0 "OK : instantane il y a ${age_h} h, ${fichiers} fichier(s), ${taille} octets."
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if (( ECHECS > 0 )); then
|
||||||
|
echo "${ECHECS} rapport(s) non deposes : Icinga ne sait donc pas ou en sont ces sauvegardes." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
@ -31,3 +31,22 @@ serveur_icinga_schema: "/usr/share/icingadb/schema/pgsql/schema.sql"
|
||||||
# contre le root_ca step-ca (host dans le SAN). root_ca doit etre lisible (0644).
|
# contre le root_ca step-ca (host dans le SAN). root_ca doit etre lisible (0644).
|
||||||
serveur_icinga_db_tls: false
|
serveur_icinga_db_tls: false
|
||||||
serveur_icinga_db_ca: "/etc/step/certs/root_ca.crt"
|
serveur_icinga_db_ca: "/etc/step/certs/root_ca.crt"
|
||||||
|
|
||||||
|
# --- Supervision des sauvegardes (resultats passifs pousses par le depot) ---
|
||||||
|
# Le controle porte sur la VERITE DE TERRAIN (l'instantane cote depot), pas sur l'unite
|
||||||
|
# systemd du noeud source : une unite verte sur un depot vide a menti pendant un mois.
|
||||||
|
serveur_icinga_setops_conf: "/etc/icinga2/conf.d/setops-sauvegardes.conf"
|
||||||
|
serveur_icinga_api_conf: "/etc/icinga2/conf.d/setops-api-users.conf"
|
||||||
|
serveur_icinga_api_utilisateur: "setops-depot"
|
||||||
|
serveur_icinga_api_motdepasse: "{{ vault_icinga_api_depot | default('') }}"
|
||||||
|
|
||||||
|
# Hote portant les depots : DERIVE du groupe, jamais ecrit en dur.
|
||||||
|
serveur_icinga_hote_sauvegarde: "{{ (groups['serveur_backup'] | default([]) | first) | default('') }}"
|
||||||
|
|
||||||
|
# Noeuds dont on ATTEND un instantane : ceux qui portent client_backup ET detiennent
|
||||||
|
# reellement de l'etat. Meme regle que `client_backup_jobs`, et meme source que P36 —
|
||||||
|
# la liste en clair du catalogue, lue depuis le role qui la possede.
|
||||||
|
serveur_icinga_sauvegarde_attendue: >-
|
||||||
|
{{ (client_backup_groupes_etat | default([]) | map('extract', groups)
|
||||||
|
| select('defined') | flatten | unique | list)
|
||||||
|
| intersect(groups['client_backup'] | default([])) }}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,13 @@ flux:
|
||||||
pair: localhost
|
pair: localhost
|
||||||
chiffrement: clair
|
chiffrement: clair
|
||||||
raison: "API Icinga 2 consommée en local par Icinga Web 2 co-localisé."
|
raison: "API Icinga 2 consommée en local par Icinga Web 2 co-localisé."
|
||||||
|
- sens: ingress
|
||||||
|
port: 5665
|
||||||
|
protocole: tcp
|
||||||
|
pair: serveur_backup
|
||||||
|
chiffrement: tls-requis
|
||||||
|
partage: true
|
||||||
|
raison: "Le depot de sauvegarde depose ses resultats passifs (portee : process-check-result sur « sauvegarde: * »)."
|
||||||
- sens: egress
|
- sens: egress
|
||||||
port: 5432
|
port: 5432
|
||||||
protocole: tcp
|
protocole: tcp
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,52 @@
|
||||||
no_log: true
|
no_log: true
|
||||||
notify: Redemarrer icingadb
|
notify: Redemarrer icingadb
|
||||||
|
|
||||||
|
# --- Supervision des sauvegardes ---
|
||||||
|
# La liste des detenteurs d'etat appartient a `client_backup`. On la LIT chez lui plutot
|
||||||
|
# que de la recopier : deux listes finissent toujours par diverger, et la divergence se
|
||||||
|
# lirait « tout va bien » des deux cotes.
|
||||||
|
- name: Lire la liste des detenteurs d'etat chez client_backup
|
||||||
|
ansible.builtin.include_vars:
|
||||||
|
file: "{{ role_path }}/../client_backup/vars/main.yml"
|
||||||
|
name: _catalogue_sauvegarde
|
||||||
|
|
||||||
|
- name: Adopter la liste des detenteurs d'etat
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
client_backup_groupes_etat: "{{ _catalogue_sauvegarde.client_backup_groupes_etat }}"
|
||||||
|
|
||||||
|
- name: Exiger le secret d'API du depot (Vault)
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- serveur_icinga_api_motdepasse | length > 0
|
||||||
|
- serveur_icinga_hote_sauvegarde | length > 0
|
||||||
|
fail_msg: >-
|
||||||
|
vault_icinga_api_depot requis, et un hote du groupe `serveur_backup` doit exister :
|
||||||
|
sans l'un ou l'autre, les sauvegardes ne seraient surveillees par personne.
|
||||||
|
|
||||||
|
- name: Deployer le compte d'API du depot de sauvegarde
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: setops-api-users.conf.j2
|
||||||
|
dest: "{{ serveur_icinga_api_conf }}"
|
||||||
|
owner: root
|
||||||
|
group: nagios
|
||||||
|
mode: "0640"
|
||||||
|
no_log: true
|
||||||
|
notify: Redemarrer icinga2
|
||||||
|
|
||||||
|
- name: Deployer les objets de supervision des sauvegardes
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: setops-sauvegardes.conf.j2
|
||||||
|
dest: "{{ serveur_icinga_setops_conf }}"
|
||||||
|
owner: root
|
||||||
|
group: nagios
|
||||||
|
mode: "0640"
|
||||||
|
notify: Redemarrer icinga2
|
||||||
|
|
||||||
|
- name: Valider la configuration Icinga 2 avant de la rendre vivante
|
||||||
|
ansible.builtin.command:
|
||||||
|
cmd: icinga2 daemon -C
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
- name: Activer et demarrer Icinga 2 et Icinga DB
|
- name: Activer et demarrer Icinga 2 et Icinga DB
|
||||||
when: not ansible_check_mode
|
when: not ansible_check_mode
|
||||||
ansible.builtin.systemd:
|
ansible.builtin.systemd:
|
||||||
|
|
|
||||||
16
roles/serveur_icinga/templates/setops-api-users.conf.j2
Normal file
16
roles/serveur_icinga/templates/setops-api-users.conf.j2
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
/*
|
||||||
|
* Gere par Set-OPS (role serveur_icinga). Ne pas editer a la main.
|
||||||
|
*
|
||||||
|
* Compte d'API dedie au DEPOT de sauvegarde, pour deposer ses resultats passifs.
|
||||||
|
* Portee minimale : uniquement `actions/process-check-result`, et uniquement sur les
|
||||||
|
* services de sauvegarde. Ce compte ne peut ni lire la configuration, ni agir ailleurs.
|
||||||
|
*/
|
||||||
|
object ApiUser "{{ serveur_icinga_api_utilisateur }}" {
|
||||||
|
password = "{{ serveur_icinga_api_motdepasse }}"
|
||||||
|
permissions = [
|
||||||
|
{
|
||||||
|
permission = "actions/process-check-result"
|
||||||
|
filter = {{ '{{' }} match("sauvegarde: *", service.name) {{ '}}' }}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
36
roles/serveur_icinga/templates/setops-sauvegardes.conf.j2
Normal file
36
roles/serveur_icinga/templates/setops-sauvegardes.conf.j2
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
/*
|
||||||
|
* Gere par Set-OPS (role serveur_icinga). Ne pas editer a la main.
|
||||||
|
*
|
||||||
|
* Supervision des sauvegardes. Le controle ne porte PAS sur l'unite systemd du noeud
|
||||||
|
* source : une unite verte sur un depot vide est exactement ce qui a menti pendant un
|
||||||
|
* mois (2026-07-03 -> 2026-08-11). Il porte sur la VERITE DE TERRAIN, cote depot :
|
||||||
|
* l'instantane du noeud existe-t-il, est-il RECENT, et n'est-il pas VIDE.
|
||||||
|
*
|
||||||
|
* Les resultats sont POUSSES par `backup-01`, qui est le seul a pouvoir lire ses depots.
|
||||||
|
* Le `ttl` porte dans chaque envoi fait la fraicheur : sans nouvelle, Icinga bascule tout
|
||||||
|
* seul en « expire ». C'est le SILENCE qui doit alerter, pas seulement l'echec — le
|
||||||
|
* silence est precisement ce qui n'a alerte personne.
|
||||||
|
*/
|
||||||
|
|
||||||
|
object CheckCommand "setops-passif" {
|
||||||
|
// Les resultats arrivent par l'API ; cette commande n'est jamais executee.
|
||||||
|
command = [ "/bin/true" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
object Host "{{ serveur_icinga_hote_sauvegarde }}" {
|
||||||
|
check_command = "hostalive"
|
||||||
|
address = "{{ serveur_icinga_hote_sauvegarde }}.{{ domaine_interne }}"
|
||||||
|
vars.role = "depot de sauvegarde"
|
||||||
|
}
|
||||||
|
|
||||||
|
{% for noeud in serveur_icinga_sauvegarde_attendue | sort %}
|
||||||
|
object Service "sauvegarde: {{ noeud }}" {
|
||||||
|
host_name = "{{ serveur_icinga_hote_sauvegarde }}"
|
||||||
|
check_command = "setops-passif"
|
||||||
|
enable_active_checks = false
|
||||||
|
enable_passive_checks = true
|
||||||
|
volatile = false
|
||||||
|
max_check_attempts = 1
|
||||||
|
vars.setops_source = "{{ noeud }}"
|
||||||
|
}
|
||||||
|
{% endfor %}
|
||||||
Loading…
Reference in a new issue