sauvegarde : le catalogue derive du groupe, et P36 le prouve
Correction : l'entree precedente attribuait le defaut a la reconstruction
from-zero. C'est faux — le commit fondateur 7476a54 (2026-07-03) disait lui-meme
« Reste : jobs Tier 1 (pg_dump/slapcat/vmail/forgejo) ». Ils n'ont jamais ete
ecrits, et infra-pki-01 a ensuite perdu son integration client_backup.
Le role qui POSSEDE la donnee dit comment la sortir : client_backup_jobs est
l'intersection du catalogue et des group_names du noeud. Un tenant qui deplace un
service emporte sa sauvegarde avec lui. On sauvegarde l'etat NON REGENERABLE :
ni zones PowerDNS ni tableaux Grafana, ils se redeploient.
L'unite qui ment est RETIREE, pas rendue bloquante : refuser le deploiement aurait
casse infra-edge-01, infra-dns-01 et mon-01, qui ne detiennent legitimement rien.
Le defaut etait le timer qui echouait chaque nuit en donnant l'apparence d'une
sauvegarde.
P36 (D-75) lit les groupes detenteurs dans le catalogue : ajouter un role au
catalogue etend la preuve du meme geste. Elle a attrape infra-pki-01 — les cles
de l'AC — corrige au plan.
Mesure hors-noeud : collab-01 64,0 MiB/272, edge-mta-01 4,4 MiB/139,
data-sql-01 1,0 MiB (pg_dumpall complet), forge-01 26,4 KiB/68,
infra-pki-01 20,1 KiB/21, idm-01 2,3 KiB/5 (slapcat). 9 hotes, 9 success.
Reste : rien ne surveille l'unite — c'est ce silence qui a laisse le defaut
vivre un mois.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
334fc65d60
commit
8eedeaf31f
7 changed files with 225 additions and 10 deletions
52
CHANGELOG.md
52
CHANGELOG.md
|
|
@ -1,5 +1,57 @@
|
||||||
# CHANGELOG — Set-OPS
|
# CHANGELOG — Set-OPS
|
||||||
|
|
||||||
|
## 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
|
||||||
|
from-zero. C'est faux. Le commit fondateur `7476a54` (2026-07-03) le disait lui-même —
|
||||||
|
*« Reste : jobs Tier 1 (pg_dump/slapcat/vmail/forgejo) »*. Ces jeux n'ont jamais été
|
||||||
|
écrits. Le Tier 0 était prouvé sur `infra-pki-01`, mais l'hôte a ensuite perdu son
|
||||||
|
intégration `client_backup` sans que rien ne le dise.
|
||||||
|
|
||||||
|
### Le catalogue vit dans le rôle, dérivé de l'appartenance aux groupes
|
||||||
|
|
||||||
|
C'est le rôle qui **possède** la donnée qui dit comment la sortir. `client_backup_jobs` est
|
||||||
|
l'intersection du catalogue et des `group_names` du nœud : un tenant qui déplace un service
|
||||||
|
emporte sa sauvegarde avec lui, sans rien redéclarer. On sauvegarde l'**état non
|
||||||
|
régénérable** — ni les zones PowerDNS ni les tableaux de bord Grafana n'y figurent, ils se
|
||||||
|
redéploient.
|
||||||
|
|
||||||
|
Les chemins ne peuvent pas référencer les defaults du rôle propriétaire : `make deployer`
|
||||||
|
déroule un play par groupe, et ceux de `serveur_forgejo` ne sont pas chargés pendant le play
|
||||||
|
de `client_backup`. D'où la forme `var | default(littéral)`.
|
||||||
|
|
||||||
|
### L'unité qui ment est retirée, pas rendue bloquante
|
||||||
|
|
||||||
|
Refuser le déploiement d'un nœud sans jeu aurait cassé `infra-edge-01`, `infra-dns-01` et
|
||||||
|
`mon-01`, qui ne détiennent légitimement rien. Le défaut n'était pas là : il était dans le
|
||||||
|
timer qui échouait chaque nuit en donnant l'apparence d'une sauvegarde. Le rôle installe
|
||||||
|
donc la sauvegarde **si et seulement si** un jeu s'applique, et **retire** celle qui
|
||||||
|
existerait. *Une sauvegarde qui ne sauvegarde rien est pire que pas de sauvegarde : elle
|
||||||
|
rassure.*
|
||||||
|
|
||||||
|
### P36 — tout détenteur d'état porte une sauvegarde
|
||||||
|
|
||||||
|
L'écart était lisible dans le plan depuis un mois (D-75). La preuve lit les groupes
|
||||||
|
détenteurs dans `client_backup_catalogue` : ajouter un rôle au catalogue étend la preuve du
|
||||||
|
même geste. Elle a immédiatement attrapé `infra-pki-01`, corrigé au plan.
|
||||||
|
|
||||||
|
### Mesuré, hors-nœud
|
||||||
|
|
||||||
|
| Hôte | Emporté |
|
||||||
|
|---|---|
|
||||||
|
| `collab-01` | 64,0 MiB · 272 fichiers |
|
||||||
|
| `edge-mta-01` | 4,4 MiB · 139 (bayes rspamd appris) |
|
||||||
|
| `data-sql-01` | 1,0 MiB · `pg_dumpall` de **toutes** les bases |
|
||||||
|
| `forge-01` | 26,4 KiB · 68 |
|
||||||
|
| `infra-pki-01` | 20,1 KiB · 21 — **les clés de l'AC** |
|
||||||
|
| `idm-01` | 2,3 KiB · 5 — l'annuaire par `slapcat` |
|
||||||
|
| `infra-mail-01`, `web-frontal-01`, `web-dorsal-01` | vides, et c'est exact : `/var/vmail`, `/srv/web` et `/srv/webapp` n'ont rien depuis la reconstruction du 2026-08-10 |
|
||||||
|
|
||||||
|
9 hôtes, 9 `success`, 5 hôtes sans sauvegarde parce qu'ils ne détiennent rien.
|
||||||
|
|
||||||
|
**Ce qui reste** : rien ne surveille encore l'unité. C'est ce silence qui a laissé le défaut
|
||||||
|
vivre un mois — Icinga devrait voir une unité systemd en échec.
|
||||||
|
|
||||||
## 2026-08-11 — En consignant l'effet du rasage, la sauvegarde s'est révélée vide
|
## 2026-08-11 — En consignant l'effet du rasage, la sauvegarde s'est révélée vide
|
||||||
|
|
||||||
Il s'agissait d'écrire une conséquence connue : raser l'hôte qui porte `openldap` détruit
|
Il s'agissait d'écrire une conséquence connue : raser l'hôte qui porte `openldap` détruit
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
> [`docs/audit/affirmations.md`](affirmations.md).
|
> [`docs/audit/affirmations.md`](affirmations.md).
|
||||||
|
|
||||||
- **Instance** : `instance` — inventaire `instance/inventories/principal/hosts.yml`
|
- **Instance** : `instance` — inventaire `instance/inventories/principal/hosts.yml`
|
||||||
- **Verdict** : ✅ CONFORME (35 OK · 0 echec · 0 saute)
|
- **Verdict** : ✅ CONFORME (36 OK · 0 echec · 0 saute)
|
||||||
|
|
||||||
## Preuves
|
## Preuves
|
||||||
|
|
||||||
|
|
@ -48,6 +48,7 @@
|
||||||
| 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). |
|
||||||
|
| P36 | Tout detenteur d'etat porte une sauvegarde | — | ✅ OK | 9 hote(s) detiennent de l'etat, tous porteurs de `client_backup` (9 groupe(s) au catalogue). |
|
||||||
|
|
||||||
## Couverture des affirmations ✅ du registre
|
## Couverture des affirmations ✅ du registre
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -84,13 +84,15 @@ seront pas recréés**. Le code ne peut pas reconstruire ce qu'il a délibérém
|
||||||
pas posséder. C'est la contrepartie exacte du régime qui protège ces décisions du prochain
|
pas posséder. C'est la contrepartie exacte du régime qui protège ces décisions du prochain
|
||||||
`make deployer`.
|
`make deployer`.
|
||||||
|
|
||||||
> **Mesure du 2026-08-11 — il n'y a rien à restaurer.** `client_backup_jobs` vaut `[]` par
|
> **Il y a désormais quelque chose à restaurer — depuis le 2026-08-11 seulement.** Jusque-là
|
||||||
> défaut et rien ne le surcharge dans l'instance. Sur 11 hôtes, `setops-sauvegarde.service`
|
> `client_backup_jobs` valait `[]` et rien ne le surchargeait : onze hôtes lançaient chaque
|
||||||
> échoue chaque nuit (`Fatal: nothing to backup`) ; sur `infra-pki-01`, `obs-01` et
|
> nuit un timer qui échouait sur `Fatal: nothing to backup`, et `infra-pki-01` — les clés de
|
||||||
> `backup-01`, aucune sauvegarde n'est déployée — et `infra-pki-01` porte les clés de l'AC.
|
> l'AC — n'avait aucune sauvegarde. `client_backup` dérive maintenant ses jeux de
|
||||||
> **Tant que ce défaut n'est pas corrigé, raser un hôte d'identité est irréversible.**
|
> l'appartenance aux groupes, et **P36** prouve statiquement que tout détenteur d'état porte
|
||||||
|
> `client_backup`. L'annuaire de `idm-01` est sorti par `slapcat` à chaque exécution.
|
||||||
|
|
||||||
Avant de raser un hôte d'identité, sortir l'annuaire à la main :
|
La sauvegarde ne dispense pas de la vérifier avant un geste destructeur. Sortir l'annuaire à
|
||||||
|
la main reste le réflexe juste avant de raser :
|
||||||
|
|
||||||
```
|
```
|
||||||
ansible <hote_openldap> -m shell -a "slapcat -b dc=<domaine> > /tmp/annuaire.ldif" -b
|
ansible <hote_openldap> -m shell -a "slapcat -b dc=<domaine> > /tmp/annuaire.ldif" -b
|
||||||
|
|
|
||||||
|
|
@ -26,5 +26,63 @@ client_backup_horaire: "*-*-* 02:30:00"
|
||||||
# nom : identifiant
|
# nom : identifiant
|
||||||
# commande : (optionnel) dump écrivant dans {{ staging }}/{{ nom }}
|
# commande : (optionnel) dump écrivant dans {{ staging }}/{{ nom }}
|
||||||
# chemins : liste de chemins à inclure dans le snapshot restic
|
# chemins : liste de chemins à inclure dans le snapshot restic
|
||||||
# Défini par nœud (group_vars). Ex. step_ca : { nom: step_ca, chemins: [/etc/step-ca] }.
|
#
|
||||||
client_backup_jobs: []
|
# CATALOGUE PAR GROUPE — c'est le rôle qui POSSEDE la donnée qui dit comment la sortir.
|
||||||
|
# On sauvegarde l'ETAT NON REGENERABLE, pas ce que le code reconstruit : ni les zones
|
||||||
|
# PowerDNS ni les tableaux de bord Grafana ne figurent ici, ils se redéploient.
|
||||||
|
#
|
||||||
|
# Les chemins reprennent les defauts du role proprietaire, mais NE PEUVENT PAS y faire
|
||||||
|
# reference : `make deployer` deroule un play par groupe, et les defaults de
|
||||||
|
# `serveur_forgejo` ne sont pas charges pendant le play de `client_backup`. D'ou la forme
|
||||||
|
# `var | default(litteral)` — la variable gagne si l'inventaire la definit, sinon le
|
||||||
|
# defaut du role proprietaire, recopie ici et a garder aligne avec lui.
|
||||||
|
client_backup_catalogue:
|
||||||
|
serveur_step_ca:
|
||||||
|
nom: step_ca
|
||||||
|
chemins:
|
||||||
|
- "{{ serveur_step_ca_steppath | default('/etc/step-ca') }}"
|
||||||
|
serveur_openldap:
|
||||||
|
nom: openldap
|
||||||
|
# slapcat lit la base a plat : pas d'authentification, coherent meme slapd arrete.
|
||||||
|
commande: >-
|
||||||
|
slapcat -b '{{ serveur_openldap_base_dn | default("dc=" + domaine_interne.split(".") | join(",dc=")) }}'
|
||||||
|
> {{ client_backup_staging }}/openldap/annuaire.ldif
|
||||||
|
chemins: ["{{ client_backup_staging }}/openldap"]
|
||||||
|
serveur_postgresql:
|
||||||
|
nom: postgresql
|
||||||
|
# pg_dumpall : TOUTES les bases + les roles et leurs mots de passe. Une base oubliee
|
||||||
|
# ici serait une base perdue — d'ou le dump global plutot qu'une liste a maintenir.
|
||||||
|
commande: >-
|
||||||
|
runuser -u postgres -- pg_dumpall --clean
|
||||||
|
> {{ client_backup_staging }}/postgresql/toutes-bases.sql
|
||||||
|
chemins: ["{{ client_backup_staging }}/postgresql"]
|
||||||
|
serveur_dovecot:
|
||||||
|
nom: courriel
|
||||||
|
chemins: ["{{ serveur_dovecot_vmail_base | default('/var/vmail') }}"]
|
||||||
|
serveur_forgejo:
|
||||||
|
nom: forgejo
|
||||||
|
chemins:
|
||||||
|
- "{{ serveur_forgejo_data | default('/var/lib/forgejo') }}"
|
||||||
|
- "{{ serveur_forgejo_config_dir | default('/etc/forgejo') }}"
|
||||||
|
serveur_nextcloud:
|
||||||
|
nom: nextcloud
|
||||||
|
chemins:
|
||||||
|
- "{{ serveur_nextcloud_data_dir | default('/var/www/nextcloud/data') }}"
|
||||||
|
- "{{ (serveur_nextcloud_racine | default('/var/www/nextcloud')) + '/config' }}"
|
||||||
|
serveur_rspamd:
|
||||||
|
# Le bayes APPRIS est de l'etat : reconstruire le noeud reinstalle rspamd, pas ce
|
||||||
|
# qu'il a appris. Les regles, elles, se redeploient et ne sont pas ici.
|
||||||
|
nom: rspamd
|
||||||
|
chemins: ["/var/lib/rspamd"]
|
||||||
|
serveur_web_frontal:
|
||||||
|
nom: web_frontal
|
||||||
|
chemins: ["{{ serveur_web_frontal_racine_base | default('/srv/web') }}"]
|
||||||
|
serveur_web_dorsal:
|
||||||
|
nom: web_dorsal
|
||||||
|
chemins: ["{{ serveur_web_dorsal_racine | default('/srv/webapp') }}"]
|
||||||
|
|
||||||
|
# Jeux retenus pour CE noeud : intersection du catalogue et de ses groupes. Derive, donc
|
||||||
|
# un tenant qui deplace un service emporte sa sauvegarde avec lui, sans rien re-declarer.
|
||||||
|
client_backup_jobs: >-
|
||||||
|
{{ client_backup_catalogue | dict2items
|
||||||
|
| selectattr('key', 'in', group_names) | map(attribute='value') | list }}
|
||||||
|
|
|
||||||
4
roles/client_backup/handlers/main.yml
Normal file
4
roles/client_backup/handlers/main.yml
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
---
|
||||||
|
- name: Recharger systemd
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
daemon_reload: true
|
||||||
|
|
@ -6,6 +6,23 @@
|
||||||
- client_backup_ssh_privkey | length > 0
|
- client_backup_ssh_privkey | length > 0
|
||||||
fail_msg: "vault_restic_password et vault_backup_ssh_privkey requis."
|
fail_msg: "vault_restic_password et vault_backup_ssh_privkey requis."
|
||||||
|
|
||||||
|
# Un noeud peut legitimement ne rien detenir de non regenerable (nginx, PowerDNS, Icinga :
|
||||||
|
# leur configuration se REDEPLOIE). Le defaut mesure le 2026-08-11 n'etait pas la, il etait
|
||||||
|
# dans l'unite qui MENT : 11 hotes deployaient un timer qui echouait chaque nuit sur
|
||||||
|
# « nothing to backup », invisible depuis le 2026-07-03. Une sauvegarde qui ne sauvegarde
|
||||||
|
# rien est pire que pas de sauvegarde — elle rassure.
|
||||||
|
#
|
||||||
|
# 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
|
||||||
|
# lisible dans le plan, donc prouve statiquement (D-75, P36) — pas ici.
|
||||||
|
- name: Etat de la sauvegarde pour ce noeud
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: >-
|
||||||
|
{{ (client_backup_jobs | length > 0)
|
||||||
|
| ternary(client_backup_jobs | length | string + ' jeu(x) : '
|
||||||
|
+ (client_backup_jobs | map(attribute='nom') | join(', ')),
|
||||||
|
'aucun etat non regenerable — aucune sauvegarde installee') }}
|
||||||
|
|
||||||
- name: Installer restic
|
- name: Installer restic
|
||||||
ansible.builtin.apt:
|
ansible.builtin.apt:
|
||||||
name: "{{ client_backup_paquets }}"
|
name: "{{ client_backup_paquets }}"
|
||||||
|
|
@ -70,6 +87,7 @@
|
||||||
mode: "0700"
|
mode: "0700"
|
||||||
|
|
||||||
- name: Déployer le script de sauvegarde
|
- name: Déployer le script de sauvegarde
|
||||||
|
when: client_backup_jobs | length > 0
|
||||||
ansible.builtin.template:
|
ansible.builtin.template:
|
||||||
src: sauvegarder.sh.j2
|
src: sauvegarder.sh.j2
|
||||||
dest: /usr/local/sbin/setops-sauvegarder.sh
|
dest: /usr/local/sbin/setops-sauvegarder.sh
|
||||||
|
|
@ -78,6 +96,7 @@
|
||||||
mode: "0700"
|
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
|
||||||
ansible.builtin.template:
|
ansible.builtin.template:
|
||||||
src: "{{ item.s }}"
|
src: "{{ item.s }}"
|
||||||
dest: "/etc/systemd/system/{{ item.d }}"
|
dest: "/etc/systemd/system/{{ item.d }}"
|
||||||
|
|
@ -89,9 +108,34 @@
|
||||||
- { s: setops-sauvegarde.timer.j2, d: setops-sauvegarde.timer }
|
- { s: setops-sauvegarde.timer.j2, d: setops-sauvegarde.timer }
|
||||||
|
|
||||||
- name: Activer le timer de sauvegarde
|
- name: Activer le timer de sauvegarde
|
||||||
when: not ansible_check_mode
|
when:
|
||||||
|
- client_backup_jobs | length > 0
|
||||||
|
- not ansible_check_mode
|
||||||
ansible.builtin.systemd:
|
ansible.builtin.systemd:
|
||||||
name: setops-sauvegarde.timer
|
name: setops-sauvegarde.timer
|
||||||
enabled: true
|
enabled: true
|
||||||
state: started
|
state: started
|
||||||
daemon_reload: true
|
daemon_reload: true
|
||||||
|
|
||||||
|
# --- Retrait d'une sauvegarde vide heritee (le noeud ne detient plus rien) ---
|
||||||
|
|
||||||
|
- name: Arreter le timer d'une sauvegarde devenue vide
|
||||||
|
when:
|
||||||
|
- client_backup_jobs | length == 0
|
||||||
|
- not ansible_check_mode
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: setops-sauvegarde.timer
|
||||||
|
enabled: false
|
||||||
|
state: stopped
|
||||||
|
failed_when: false
|
||||||
|
|
||||||
|
- name: Retirer le script et les unites d'une sauvegarde vide
|
||||||
|
when: client_backup_jobs | length == 0
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ item }}"
|
||||||
|
state: absent
|
||||||
|
loop:
|
||||||
|
- /usr/local/sbin/setops-sauvegarder.sh
|
||||||
|
- /etc/systemd/system/setops-sauvegarde.service
|
||||||
|
- /etc/systemd/system/setops-sauvegarde.timer
|
||||||
|
notify: Recharger systemd
|
||||||
|
|
|
||||||
|
|
@ -472,6 +472,58 @@ def preuve_base_par_consommateur() -> tuple[bool, str]:
|
||||||
f"({len(bases)} entree(s) au registre).")
|
f"({len(bases)} entree(s) au registre).")
|
||||||
|
|
||||||
|
|
||||||
|
def preuve_etat_sauvegarde() -> tuple[bool, str]:
|
||||||
|
"""Tout hote qui detient de l'etat non regenerable porte `client_backup`.
|
||||||
|
|
||||||
|
Pourquoi statiquement. Le 2026-08-11, la mesure a montre que RIEN n'etait sauvegarde
|
||||||
|
dans l'ecosysteme : onze hotes lancaient chaque nuit un timer qui echouait sur
|
||||||
|
« nothing to backup », et `infra-pki-01` — qui porte les CLES DE L'AC, et qui avait
|
||||||
|
servi a prouver le Tier 0 le 2026-07-03 — n'avait aucune sauvegarde du tout. L'ecart
|
||||||
|
etait entierement lisible dans le plan depuis un mois. D-75 : ce qui se lit
|
||||||
|
statiquement se prouve statiquement, sinon on l'apprend le jour de la restauration.
|
||||||
|
|
||||||
|
RIEN N'EST CODE EN DUR : la liste des groupes detenteurs d'etat est LUE dans
|
||||||
|
`client_backup_catalogue` (defaults du role). Ajouter un role au catalogue etend donc
|
||||||
|
la preuve du meme geste — on ne peut pas declarer une donnee sauvegardable et oublier
|
||||||
|
d'exiger qu'elle le soit.
|
||||||
|
|
||||||
|
CE QU'ELLE NE TESTE PAS : que la sauvegarde s'execute, ni qu'elle contienne quoi que
|
||||||
|
ce soit. Une unite verte sur un depot vide resterait invisible ici — c'est au reel de
|
||||||
|
le dire, pas au depot.
|
||||||
|
"""
|
||||||
|
plan = RACINE / "instance" / "plan"
|
||||||
|
defauts = RACINE / "roles" / "client_backup" / "defaults" / "main.yml"
|
||||||
|
if not (plan / "serveurs.yml").is_file() or not defauts.is_file():
|
||||||
|
return True, "Aucun plan ou aucun role client_backup : rien a verifier."
|
||||||
|
# Le catalogue porte du Jinja : on ne lit que les CLES, sans rendre les valeurs.
|
||||||
|
catalogue = set(re.findall(r"^ (serveur_[a-z0-9_]+):\s*$",
|
||||||
|
defauts.read_text(encoding="utf-8"), re.M))
|
||||||
|
if not catalogue:
|
||||||
|
return False, ("`client_backup_catalogue` est vide ou illisible : aucun groupe "
|
||||||
|
"detenteur d'etat n'est declare.")
|
||||||
|
apps = (yaml.safe_load((plan / "applications.yml").read_text(encoding="utf-8"))
|
||||||
|
or {}).get("applications") or {}
|
||||||
|
serveurs = (yaml.safe_load((plan / "serveurs.yml").read_text(encoding="utf-8"))
|
||||||
|
or {}).get("serveurs") or {}
|
||||||
|
|
||||||
|
detenu: dict[str, set[str]] = {}
|
||||||
|
for app in apps.values():
|
||||||
|
groupe = str((app or {}).get("groupe") or "")
|
||||||
|
if groupe in catalogue:
|
||||||
|
detenu.setdefault(str((app or {}).get("hote") or ""), set()).add(groupe)
|
||||||
|
|
||||||
|
manques = [f"{hote} detient {sorted(groupes)}"
|
||||||
|
for hote, groupes in sorted(detenu.items())
|
||||||
|
if "client_backup" not in ((serveurs.get(hote) or {}).get("integrations") or [])]
|
||||||
|
if manques:
|
||||||
|
return False, (f"{len(manques)} hote(s) detiennent de l'etat sans sauvegarde : "
|
||||||
|
+ " | ".join(manques)
|
||||||
|
+ " — ajouter `client_backup` a leurs `integrations` dans "
|
||||||
|
"`plan/serveurs.yml`.")
|
||||||
|
return True, (f"{len(detenu)} hote(s) detiennent de l'etat, tous porteurs de "
|
||||||
|
f"`client_backup` ({len(catalogue)} groupe(s) au catalogue).")
|
||||||
|
|
||||||
|
|
||||||
def preuve_lecteur_declare() -> tuple[bool, str]:
|
def preuve_lecteur_declare() -> tuple[bool, str]:
|
||||||
"""Chaque document de `docs/` declare son lecteur des sa premiere ligne.
|
"""Chaque document de `docs/` declare son lecteur des sa premiere ligne.
|
||||||
|
|
||||||
|
|
@ -722,6 +774,8 @@ PREUVES: list[dict] = [
|
||||||
"func": preuve_lecteur_declare},
|
"func": preuve_lecteur_declare},
|
||||||
{"id": "P35", "titre": "Toute application exigeant une base en a une au plan", "refs": [],
|
{"id": "P35", "titre": "Toute application exigeant une base en a une au plan", "refs": [],
|
||||||
"func": preuve_base_par_consommateur},
|
"func": preuve_base_par_consommateur},
|
||||||
|
{"id": "P36", "titre": "Tout detenteur d'etat porte une sauvegarde", "refs": [],
|
||||||
|
"func": preuve_etat_sauvegarde},
|
||||||
{"id": "P33", "titre": "Aucune collision de port entre roles co-localises", "refs": [],
|
{"id": "P33", "titre": "Aucune collision de port entre roles co-localises", "refs": [],
|
||||||
"cmds": [[sys.executable, "scripts/verifier_ports.py"]]},
|
"cmds": [[sys.executable, "scripts/verifier_ports.py"]]},
|
||||||
]
|
]
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue