Compare commits
7 commits
v2026.10.0
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 479ec3cc7d | |||
| 9a4b63544e | |||
| 3e2846cc5f | |||
| cca22f47ca | |||
| 4660a8892a | |||
| 6141bc4b2f | |||
| e8907285d5 |
18 changed files with 900 additions and 15 deletions
234
CHANGELOG.md
234
CHANGELOG.md
|
|
@ -1,5 +1,239 @@
|
|||
# CHANGELOG — Set-OPS
|
||||
|
||||
## 2026-10-07 (110) — Les 13 s de Loki n'étaient pas la copie : c'était son arrêt, qui attend un envoi d'Alloy
|
||||
|
||||
**94 preuves.** Correction de l'entrée (109). Déployée partout (`client_backup` chez les deux
|
||||
locataires et au site, simulé d'abord, 0 échec), la liaison des morceaux a été mesurée sur un
|
||||
dépôt ordinaire lancé sur `site-mon-01` : Loki est encore resté **10,8 s** à l'arrêt.
|
||||
|
||||
### Ce que j'avais mesuré, et ce que je n'avais pas mesuré
|
||||
|
||||
J'avais chronométré la COPIE à part (1,5 s à chaud, 13 s pendant la sauvegarde), jamais
|
||||
l'ARRÊT. Le journal de systemd tranche : « Stopping » à 23:47:25.65, « Stopped » à
|
||||
23:47:36.40, soit **10,75 s pour s'arrêter**. La copie, liée, prend environ 60 ms : « Started »
|
||||
à 23:47:36.46. Le morceau vérifié a bien deux liens (la base et la copie).
|
||||
|
||||
**Pourquoi 10 s** : le journal de Loki montre `POST /loki/api/v1/push (500) 10.000656983s`.
|
||||
À l'arrêt, Loki attend la fin des requêtes en cours ; un envoi d'un agent Alloy n'a abouti
|
||||
qu'à l'expiration du délai d'Alloy (10 s par défaut), en 500. Alloy réessaie un 500.
|
||||
|
||||
**Rien de perdu**, mesuré : aucune tranche de 30 s vide dans Loki autour de l'arrêt (3 058 et
|
||||
3 984 lignes à 23:47, avec le rattrapage d'Alloy) ; Prometheus, un seul relevé manqué (30 s).
|
||||
|
||||
**Laissé tel quel** : raccourcir cet arrêt voudrait dire tuer Loki avant la fin de son arrêt
|
||||
propre, ou baisser le délai d'Alloy en temps normal. Deux risques pour gagner dix secondes
|
||||
par nuit, sans aucune perte à rattraper. La liaison reste : elle retire 1,5 à 2 s de copie et
|
||||
la place en double.
|
||||
|
||||
## 2026-10-07 (109) — Les morceaux de Loki sont liés, pas copiés ; sa base de suppressions, si
|
||||
|
||||
**94 preuves.** Au site, la copie à froid arrêtait Loki **13 s**, pour copier 6 550 petits
|
||||
fichiers. Ses morceaux de journal sont adressés par leur contenu : un nom décodé donne
|
||||
`1a0d123a4e8:1a0d1918235:6b5feaac` (début, fin, somme de contrôle). Ils ne bougent donc
|
||||
jamais, et les lier prend **40 ms** (mesuré sur `site-mon-01`).
|
||||
|
||||
### Le fichier qui bougeait parmi les morceaux
|
||||
|
||||
Un contrôle d'immuabilité, refait avec `stat` (le `find` de la machine ne connaît pas `%W` :
|
||||
le premier passage ne mesurait rien), a trouvé **un fichier sur 6 550** modifié après sa
|
||||
naissance : `chunks/index/delete_requests/delete_requests.gz`, né le 2026-09-12 et réécrit le
|
||||
2026-10-07. Ce n'est pas un morceau : c'est la base des demandes de suppression du compacteur,
|
||||
rangée dans le même stockage objet. Lier tout `chunks/` aurait laissé la sauvegarde changer
|
||||
avec elle.
|
||||
|
||||
**Fait** : `setops-copie-a-froid` accepte `--lier REL` (lier tout un sous-dossier) et
|
||||
`--copier REL` (y copier vraiment ce qui bouge). Loki : `--lier chunks --copier chunks/index`.
|
||||
|
||||
**Éprouvé** : `test_restauration.py` ; morceau lié (même inode), `chunks/index` copié (autre
|
||||
inode), une réécriture après la copie ne touche pas la sauvegarde. Témoin : sans `--copier`,
|
||||
les deux derniers échouent. `shellcheck` propre. `make verifier` conforme, **94/94**.
|
||||
|
||||
**Pas encore déployé** : la durée réelle se mesurera au prochain dépôt.
|
||||
|
||||
## 2026-10-07 (108) — Le site recevait encore la sauvegarde d'avant le 2026-09-30
|
||||
|
||||
**94 preuves.** Déployer la copie à froid de Prometheus et Loki au site (`site-mon-01`) a
|
||||
révélé que les trois machines sauvegardées du site n'avaient **pas `setops-restaurer`** :
|
||||
leur `client_backup` datait d'avant la restauration (2026-09-30). Ni garde, ni étiquette
|
||||
`avant-raser`, ni témoins. Ce qui était cohérent avec un site jamais reconstruit, mais pas
|
||||
avec un site qui doit pouvoir l'être.
|
||||
|
||||
### Simulé, puis déployé (confirmé par l'exploitant)
|
||||
|
||||
- **La garde ne bloquerait rien** : machines nées le 2026-09-12 vers 20:20, plus anciens
|
||||
instantanés le 2026-09-13 à 02:3x, donc aucune incarnation précédente. Chaque jeu est acté
|
||||
« neuf ».
|
||||
- `make site-appliquer GROUPE=client_backup` (`--check --diff` d'abord) : 0 échec sur
|
||||
`site-mon-01`, `site-forge-01`, `site-pki-01` ; premier rapport complet. Jeux :
|
||||
`postgresql`, `prometheus`, `loki`, `icinga` (mon) ; `forgejo` (forge, déjà « en place »
|
||||
depuis le 2026-10-04) ; `step_ca` (pki).
|
||||
- `serveur_prometheus`, `serveur_loki`, `serveur_icinga` **non redéployés** au site : leur
|
||||
restauration ne sert qu'à une reconstruction, et `serveur_icinga` aurait embarqué un retard
|
||||
sans rapport (catalogue des capteurs du 2026-10-04, jamais posé au site).
|
||||
|
||||
### Mesuré
|
||||
|
||||
| `site-mon-01` | |
|
||||
|---|---|
|
||||
| Prometheus (2,9 Go) arrêté | 523 ms ; écart maximal entre deux échantillons : 30 s (normal 15 s) |
|
||||
| Loki (285 Mo, 6 512 fichiers) arrêté | **13 074 ms** : la copie de petits fichiers, pas la lecture (2 s) |
|
||||
| Premier dépôt | 3,16 Gio (1,57 Gio stockés) |
|
||||
|
||||
Pendant les 13 s de Loki : 0 erreur, 0 abandon chez les 12 agents Alloy du site, et **aucune
|
||||
tranche de 30 s vide** dans Loki (`count_over_time`, de 23:11 à 23:16) ; celle de l'arrêt
|
||||
compte 2 538 lignes. Les journaux émis pendant l'arrêt sont arrivés, avec leur heure.
|
||||
|
||||
**À considérer** : les morceaux de Loki sont immuables, comme les blocs de Prometheus. Les
|
||||
lier au lieu de les copier ramènerait son arrêt sous la seconde.
|
||||
|
||||
**Hors périmètre, remarqué** : le catalogue des capteurs d'Icinga (accents, champs
|
||||
`action`, 2026-10-04) n'a jamais été déployé au site.
|
||||
|
||||
## 2026-10-07 (107) — Les métriques et les journaux repartaient de zéro : copie à froid de Prometheus et Loki
|
||||
|
||||
**94 preuves.** Prometheus et Loki n'étaient pas sauvegardés : chaque reconstruction
|
||||
effaçait les métriques (15 jours) et les journaux (31 jours). Décision de l'exploitant : les
|
||||
sauvegarder, avec un arrêt bref plutôt qu'une copie à chaud (« on ne prend pas de risque »).
|
||||
|
||||
### Mesuré avant d'écrire
|
||||
|
||||
Au site (`site-mon-01`, jamais reconstruit) : Prometheus **2,9 Go** (22 blocs), Loki **285 Mo**
|
||||
(6 512 fichiers) ; chez les locataires, reconstruits le jour même, 41 à 98 Mo et 10 Mo. Seuls
|
||||
le WAL de Prometheus (178 Mo) et `chunks_head` (21 Mo) bougent ; le reste est fait de blocs
|
||||
immuables. Lecture réelle : 4,2 s pour tout Prometheus, 0,3 s pour son WAL, 2 s pour Loki. Un
|
||||
premier chronométrage disait 6 ms pour 2,9 Go : GNU tar ne lit rien quand il écrit vers
|
||||
`/dev/null`. C'est l'instrument qui mentait, pas le disque.
|
||||
|
||||
### Fait
|
||||
|
||||
- **`setops-copie-a-froid`** (client_backup) : arrête le service, copie, le relance **quoi
|
||||
qu'il arrive** et seulement s'il tournait ; la copie précédente n'est remplacée qu'une fois
|
||||
la nouvelle complète. Les blocs de Prometheus (dossiers ULID) sont liés, le WAL et
|
||||
`chunks_head` copiés. Il dit la durée d'arrêt.
|
||||
- **Jeux `prometheus` et `loki`** au catalogue : la copie à froid va dans
|
||||
`/var/backups/setops/<jeu>`, et restic l'emporte. `vivant` dit où vit la base.
|
||||
- **Restauration** dans `serveur_prometheus` (juste après le paquet, base sans bloc) et
|
||||
`serveur_loki` (avant le démarrage, base sans morceau de journal) : service arrêté,
|
||||
`setops-restaurer fichiers --depuis <copie> <base vivante>`, acté.
|
||||
- **Témoins** : Prometheus, un bloc vivant doit couvrir le début du dernier bloc d'avant
|
||||
(la rétention ne l'a pas retiré, une base non restaurée ne peut pas le couvrir) ; sans
|
||||
bloc, c'est le WAL qui sert de repère. Loki : aucun morceau d'avant subsistant = écart ; une
|
||||
perte partielle (rétention) est listée.
|
||||
- **`obs-01` entre dans `client_backup`** chez les deux locataires (P36 l'exigeait :
|
||||
« obs-01 détient serveur_loki, serveur_prometheus sans sauvegarde »). Plan, inventaire
|
||||
régénéré (`instancier-appliquer FORCE=1`, confirmé par l'exploitant), flux (`mon-01` accepte
|
||||
désormais les rapports d'`obs-01` sur 5665), faces republiées.
|
||||
|
||||
### Éprouvé
|
||||
|
||||
`test_temoins.py` : blocs restaurés puis fusionnés, base née après la reconstruction (écart),
|
||||
rétention, WAL seul, Loki restauré, partiel, absent (écart). `test_restauration.py` :
|
||||
`--depuis`, et la copie à froid avec un faux `systemctl` (arrêt, copie, relance ; bloc lié,
|
||||
WAL copié ; copie ratée : service **relancé quand même**, copie précédente intacte ; service
|
||||
inactif laissé tel quel). Témoin : sans la relance de secours, le test échoue. `shellcheck`
|
||||
propre. `make verifier` conforme, **94/94**.
|
||||
|
||||
### Déployé, mesuré (actions confirmées par l'exploitant)
|
||||
|
||||
Ajouter un détenteur d'état ouvre des flux, et l'ordre compte :
|
||||
- **Frontière** : rien à faire (`frontiere-plan` : 0 à créer, 0 à retirer).
|
||||
- **Pare-feu Proxmox** (`proxmox-fw-appliquer`, depuis le runner du site) : `t17-cli-backup`
|
||||
et `t23-cli-backup` gagnent chacun `obs-01` (10 -> 11) ; relu, rien à faire ensuite. Le
|
||||
devis exige encore un locataire monté sur le runner (`Inventaire introuvable sous
|
||||
…/instance`) : lancé avec `SETOPS_INSTANCE=/opt/setops/OPS-Technolibre`. C'est un reste de
|
||||
l'étape 3 des contextes.
|
||||
- **nftables de `mon-01`** (`serveur_durci --limit mon-01`, simulé d'abord) : seule la règle
|
||||
5665 gagne `obs-01` ; relu dans le jeu de règles chargé, sur les deux locataires.
|
||||
- **Puis** `serveur_icinga`, `client_backup`, `serveur_prometheus`, `serveur_loki` : 0 échec ;
|
||||
premier rapport complet d'`obs-01` ; jeux `prometheus` et `loki` actés « neufs » par le
|
||||
premier dépôt (aucun instantané antérieur), donc rien n'a été écrasé.
|
||||
|
||||
**Le trou de collecte, mesuré** (écart maximal entre deux échantillons de `up`, sur 15
|
||||
cibles, autour de l'arrêt) :
|
||||
|
||||
| | Prometheus arrêté | Loki arrêté | écart maximal |
|
||||
|---|---|---|---|
|
||||
| Chezlepro | 151 ms | 614 ms | 30 s (intervalle normal : 15 s) |
|
||||
| Technolibre | 85 ms | 582 ms | 30 s |
|
||||
|
||||
Un seul relevé manqué par cible. Premier dépôt : 146 Mo (43 Mo stockés) et 124 Mo (40 Mo).
|
||||
|
||||
**Pas encore éprouvé** : la restauration elle-même (prochaine reconstruction). Le site
|
||||
(`site-mon-01`, 2,9 Go, WAL de 178 Mo) n'est pas déployé ; son rejeu de WAL sera plus long :
|
||||
simuler avant.
|
||||
|
||||
**Hors périmètre, remarqué** : `make flux` a réécrit les 9 `connectivite.json` du site (+188
|
||||
lignes, `sorties_externes`). Le générateur les produit depuis `d6591cb` (2026-10-05), mais les
|
||||
fichiers du site datent du 2026-10-01. Changement annulé ici, à régénérer à part.
|
||||
|
||||
## 2026-10-07 (106) — Le gabarit 99998 sort des déclarations
|
||||
|
||||
**94 preuves.** L'exploitant a déclaré obsolète l'ancien gabarit, `99998`. Le neuf (`9006`,
|
||||
sur `CephNVMe`) a cloné les cinq reconstructions complètes du 2026-10-07.
|
||||
|
||||
### Deux déclarations le nommaient encore
|
||||
|
||||
- **`OPS-Technolibre/inventories/principal/group_vars/proxmox.yml`** :
|
||||
`proxmox_clone_vmid_modele: 99998` et `proxmox_clone_source_nom: modeleSetOPS`. Plus rien
|
||||
ne les clonait (`cloner-vm` passe le gabarit du site en `-e`, entrée (102)). Mises en
|
||||
commentaire, comme chez Chezlepro depuis le 2026-09-01 ; la face de Technolibre
|
||||
republiée (`make face-reseau-publier`) perd exactement ces deux lignes.
|
||||
- **`SITE-Chezlepro/plan/10-intrants.yml`** : `gabarit.precedent: 99998`, « gardé le temps
|
||||
de prouver le neuf sur une reconstruction complète ». Aucun script ne le lisait. Retiré.
|
||||
Les fiches que le site dépose chez ses locataires (`make fiches-site-deposer`) ne changent
|
||||
que par l'empreinte de ce fichier ; leur contenu est identique.
|
||||
|
||||
**Validation** : `make verifier` conforme, **94/94**. Un premier passage a échoué, et c'était
|
||||
juste : P91 refusait les deux `fiche-site.yml`, périmées par l'empreinte de leur source.
|
||||
|
||||
**Reste** : la VM `99998` est toujours sur le cluster. La supprimer touche un hyperviseur
|
||||
(R4) : décision à part.
|
||||
|
||||
## 2026-10-07 (105) — Les releases et le wiki n'allaient que là où on les poussait à la main
|
||||
|
||||
**94 preuves.** La forge du site fait autorité (D-81), mais deux choses n'y arrivaient
|
||||
jamais d'elles-mêmes : les étiquettes de release, et le wiki.
|
||||
|
||||
### Les releases manquaient à la forge du site
|
||||
|
||||
`v2026.10.07` poussée sur eregion, la question suivante a été : et la forge du site ? Mesure
|
||||
(`git ls-remote --tags`) : elle portait `v2026.10.04`, pas `v2026.08.21` ni `v2026.10.07`. Le
|
||||
colis de `make publier` (`genome_colis.py`) ne transporte que `refs/heads/<branche>`, et
|
||||
`publier.py` ne poussait que la branche vers eregion. `v2026.10.04` y avait été portée par un
|
||||
geste à part, que rien ne gardait. Les deux étiquettes manquantes ont été poussées par
|
||||
`origin` (rebond SSH du poste), et relues identiques des deux côtés.
|
||||
|
||||
**Fait** :
|
||||
- `genome_colis.py` : le colis emporte toutes les étiquettes (`bundle create … --tags`) ; le
|
||||
runner les récupère et les pousse avec la branche, **sans forcer** : une étiquette déjà
|
||||
présente sous un autre objet est refusée, des deux côtés. Une release ne bouge pas.
|
||||
- `genome_pousser.yml` : la forge est relue par son API (`git/refs/tags`), et doit porter
|
||||
toutes les étiquettes du poste.
|
||||
- `publier.py` pousse aussi les étiquettes vers eregion.
|
||||
|
||||
**Option écartée** : faire d'eregion un miroir poussé par la forge du site. Mesuré : eregion
|
||||
porte deux branches de contribution non fusionnées (`essai_integration`, 4 commits ;
|
||||
`fix_client_pki`, demande de fusion n° 5 ouverte), qu'un miroir effacerait. L'exploitant
|
||||
garde les deux forges poussées depuis le poste, par prudence envers les contributions.
|
||||
|
||||
**Éprouvé** : `test_genome_colis.py` (dans `make test`), sur de vrais dépôts git jetables :
|
||||
sans étiquette, la tête passe ; une release annotée arrive sur la forge, même objet ; une
|
||||
étiquette déplacée est refusée, la forge garde l'ancienne. Témoin : sur l'ancien
|
||||
`genome_colis.py`, le test échoue dès « la release voyage jusqu'à la forge ». `make
|
||||
verifier` conforme, **94/94**.
|
||||
|
||||
**Pas encore éprouvé** : la relecture par l'API de la forge (`git/refs/tags`). Le premier
|
||||
`make publier` qui suivra le dira : « N etiquette(s) du poste, toutes sur la forge ».
|
||||
|
||||
### Le wiki de la forge du site était vide
|
||||
|
||||
`make wiki-publier` publie vers UNE forge (`WIKI_REMOTE`) ; son témoin
|
||||
(`docs/audit/wiki-publie.yml`) montrait la dernière publication, le 2026-09-29, vers eregion
|
||||
seulement. Rien n'avait jamais publié vers la forge du site. Sa branche de wiki demandée à
|
||||
l'API de Forgejo (`wiki_branch: main`), le wiki est publié sur les deux forges ; relus :
|
||||
même arbre (`b3a70ba`), 96 pages, 8 figures, des deux côtés. Le témoin ne retient qu'une
|
||||
forge (la dernière publiée, eregion) : P60 ne voit pas l'autre.
|
||||
|
||||
## 2026-10-07 (104) — L'historique de supervision traverse la reconstruction : les deux locataires, sur `2cbfa2d`
|
||||
|
||||
**94 preuves.** Les deux locataires rasés et reconstruits par le site qui les nomme, sur le
|
||||
|
|
|
|||
1
Makefile
1
Makefile
|
|
@ -318,6 +318,7 @@ test: ## Lance les tests unitaires (derivation de nomenclature et d'inventaire)
|
|||
python3 scripts/tests/test_raser.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_adressage_derive.py
|
||||
python3 scripts/tests/test_gui_intrants.py
|
||||
python3 scripts/tests/test_runbooks.py
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
# 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
|
||||
source: 3fe7e3c
|
||||
date: 2026-09-29
|
||||
source: e9215a2
|
||||
date: 2026-10-07
|
||||
|
|
|
|||
|
|
@ -160,6 +160,7 @@
|
|||
loop: "{{ genome_a_pousser }}"
|
||||
loop_control:
|
||||
label: "{{ item.dest }}"
|
||||
register: genome_emballe
|
||||
changed_when: true
|
||||
|
||||
- name: Porter les colis jusqu'au runner
|
||||
|
|
@ -255,6 +256,45 @@
|
|||
label: "{{ item.depot }}"
|
||||
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).
|
||||
#
|
||||
# C'est LUI qui pousse, donc il doit d'abord recevoir : ce playbook fait avancer le
|
||||
|
|
|
|||
|
|
@ -83,6 +83,29 @@ client_backup_catalogue:
|
|||
# PAS DE `serveur_web_frontal` (retire le 2026-09-30) : le frontal RELAIE, il ne sert
|
||||
# rien lui-meme — ses vhosts, son WAF et sa page 404 se redeploient depuis le depot.
|
||||
# `/srv/web` venait de l'epoque ou il servait du statique ; ses instantanes etaient vides.
|
||||
# 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
|
||||
|
|
|
|||
84
roles/client_backup/files/setops-copie-a-froid.sh
Normal file
84
roles/client_backup/files/setops-copie-a-froid.sh
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
#!/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")"
|
||||
|
|
@ -19,7 +19,8 @@ VERDICTS, PAR JEU :
|
|||
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. Exactement ce que la reconstruction du
|
||||
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.
|
||||
|
|
@ -30,6 +31,7 @@ import argparse
|
|||
import base64
|
||||
import collections
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
|
|
@ -174,6 +176,75 @@ def instance_id(r: Rapport, avant: Path, vivant: Path) -> None:
|
|||
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")
|
||||
a = set(arbre(avant / "chunks"))
|
||||
v = set(arbre(vivant / "chunks"))
|
||||
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]]]:
|
||||
|
|
@ -317,6 +388,8 @@ def main(argv: list[str] | None = None, interroger=psql) -> int:
|
|||
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é")
|
||||
|
|
@ -327,9 +400,17 @@ def main(argv: list[str] | None = None, interroger=psql) -> int:
|
|||
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("=")
|
||||
if nom == "openldap":
|
||||
# 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)"
|
||||
|
|
|
|||
|
|
@ -56,6 +56,17 @@
|
|||
group: root
|
||||
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
|
||||
when: client_backup_jobs | length > 0
|
||||
ansible.builtin.template:
|
||||
|
|
@ -116,6 +127,7 @@
|
|||
state: absent
|
||||
loop:
|
||||
- /usr/local/sbin/setops-sauvegarder.sh
|
||||
- /usr/local/sbin/setops-copie-a-froid
|
||||
- /etc/systemd/system/setops-sauvegarde.service
|
||||
- /etc/systemd/system/setops-sauvegarde.timer
|
||||
- /etc/systemd/system/setops-sauvegarde-avant-raser.service
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@
|
|||
# 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
|
||||
|
|
@ -58,6 +60,9 @@ 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; }
|
||||
|
|
@ -154,22 +159,28 @@ vide() { # vrai si le chemin est absent, ou un repertoire sans contenu
|
|||
}
|
||||
|
||||
cmd_fichiers() {
|
||||
local proprio="" remplacer=0 vers="" chemins=() src dest n
|
||||
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
|
||||
extraire "${chemins[@]}"
|
||||
if [[ -n "${depuis}" ]]; then extraire "${depuis}"; else extraire "${chemins[@]}"; fi
|
||||
for c in "${chemins[@]}"; do
|
||||
src="${TMP}${c}"
|
||||
src="${TMP}${depuis:-${c}}"
|
||||
dest="${vers}${c}"
|
||||
if [[ ! -e "${src}" ]]; then echo "ABSENT de ${INSTANTANE} : ${c}"; continue; fi
|
||||
if ! vide "${dest}"; then
|
||||
|
|
@ -358,6 +369,7 @@ cmd_temoins() {
|
|||
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
|
||||
|
|
|
|||
|
|
@ -21,3 +21,5 @@ client_backup_groupes_etat:
|
|||
- serveur_rspamd
|
||||
- serveur_web_dorsal
|
||||
- serveur_icinga
|
||||
- serveur_prometheus
|
||||
- serveur_loki
|
||||
|
|
|
|||
|
|
@ -195,6 +195,56 @@
|
|||
group: loki
|
||||
mode: "0750"
|
||||
|
||||
# --- LA BASE D'UNE INCARNATION PRECEDENTE (2026-10-07) --------------------------------
|
||||
#
|
||||
# Les journaux repartaient de zero a chaque reconstruction. client_backup en garde une
|
||||
# copie a froid (jeu `loki`) ; on la remet ICI, avant que Loki ne demarre sur sa
|
||||
# configuration : une base sans aucun morceau de journal est une base neuve. Service
|
||||
# arrete pendant la remise ; « Activer et demarrer Loki » plus bas le relance.
|
||||
- name: La base de Loki a-t-elle deja des morceaux de journal ?
|
||||
ansible.builtin.find:
|
||||
paths: "{{ serveur_loki_chemin }}/chunks"
|
||||
recurse: true
|
||||
register: serveur_loki_morceaux
|
||||
|
||||
- name: Restauration — la base d'une incarnation precedente ?
|
||||
ansible.builtin.include_role:
|
||||
name: client_backup
|
||||
tasks_from: restaurer.yml
|
||||
vars:
|
||||
client_backup_restaurer_jeu: serveur_loki
|
||||
client_backup_restaurer_vierge: "{{ serveur_loki_morceaux.matched == 0 }}"
|
||||
|
||||
- name: Restauration — remettre la base, service arrete
|
||||
when: client_backup_restauration.etat == 'a_restaurer'
|
||||
block:
|
||||
- name: Restauration — arreter Loki
|
||||
ansible.builtin.systemd:
|
||||
name: "{{ serveur_loki_service }}"
|
||||
state: stopped
|
||||
|
||||
- name: Restauration — remettre la copie a froid a sa place
|
||||
changed_when: true
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/local/sbin/setops-restaurer
|
||||
- fichiers
|
||||
- --instantane
|
||||
- "{{ client_backup_restauration.instantane }}"
|
||||
- --depuis
|
||||
- "{{ client_backup_restauration.chemins[0] }}/donnees"
|
||||
- --remplacer
|
||||
- --proprietaire
|
||||
- loki:loki
|
||||
- "{{ serveur_loki_chemin }}"
|
||||
|
||||
- name: Restauration — acter la base remise
|
||||
ansible.builtin.include_role:
|
||||
name: client_backup
|
||||
tasks_from: acter.yml
|
||||
vars:
|
||||
client_backup_acter_etat: restaure
|
||||
|
||||
# --- TLS : synchroniser le cert step_ca AVANT d'activer http_tls_config ---
|
||||
- name: TLS — repertoire des certificats (loki)
|
||||
ansible.builtin.file:
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ serveur_prometheus_paquets:
|
|||
|
||||
serveur_prometheus_service: "prometheus"
|
||||
serveur_prometheus_config: "/etc/prometheus/prometheus.yml"
|
||||
# Le dossier de la base (defaut du paquet Debian, que `ARGS` ne change pas). Lu aussi par
|
||||
# client_backup : la copie a froid et la restauration le visent.
|
||||
serveur_prometheus_donnees: "/var/lib/prometheus/metrics2"
|
||||
|
||||
serveur_prometheus_intervalle: "15s"
|
||||
serveur_prometheus_retention: "15d"
|
||||
|
|
|
|||
|
|
@ -6,6 +6,58 @@
|
|||
update_cache: true
|
||||
cache_valid_time: 3600
|
||||
|
||||
# --- LA BASE D'UNE INCARNATION PRECEDENTE (2026-10-07) --------------------------------
|
||||
#
|
||||
# Les metriques repartaient de zero a chaque reconstruction. client_backup en garde une
|
||||
# copie a froid (jeu `prometheus`) ; on la remet ICI, juste apres l'installation : le paquet
|
||||
# demarre Prometheus aussitot, mais sa base n'a encore aucun bloc (le premier se ferme au
|
||||
# bout de deux heures). Service arrete pendant la remise ; la tache « Activer et demarrer »
|
||||
# plus bas le relance.
|
||||
- name: La base de Prometheus a-t-elle deja des blocs ?
|
||||
ansible.builtin.find:
|
||||
paths: "{{ serveur_prometheus_donnees }}"
|
||||
file_type: directory
|
||||
patterns: "01*"
|
||||
register: serveur_prometheus_blocs
|
||||
|
||||
- name: Restauration — la base d'une incarnation precedente ?
|
||||
ansible.builtin.include_role:
|
||||
name: client_backup
|
||||
tasks_from: restaurer.yml
|
||||
vars:
|
||||
client_backup_restaurer_jeu: serveur_prometheus
|
||||
client_backup_restaurer_vierge: "{{ serveur_prometheus_blocs.matched == 0 }}"
|
||||
|
||||
- name: Restauration — remettre la base, service arrete
|
||||
when: client_backup_restauration.etat == 'a_restaurer'
|
||||
block:
|
||||
- name: Restauration — arreter Prometheus
|
||||
ansible.builtin.systemd:
|
||||
name: "{{ serveur_prometheus_service }}"
|
||||
state: stopped
|
||||
|
||||
- name: Restauration — remettre la copie a froid a sa place
|
||||
changed_when: true
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/local/sbin/setops-restaurer
|
||||
- fichiers
|
||||
- --instantane
|
||||
- "{{ client_backup_restauration.instantane }}"
|
||||
- --depuis
|
||||
- "{{ client_backup_restauration.chemins[0] }}/metrics2"
|
||||
- --remplacer
|
||||
- --proprietaire
|
||||
- prometheus:prometheus
|
||||
- "{{ serveur_prometheus_donnees }}"
|
||||
|
||||
- name: Restauration — acter la base remise
|
||||
ansible.builtin.include_role:
|
||||
name: client_backup
|
||||
tasks_from: acter.yml
|
||||
vars:
|
||||
client_backup_acter_etat: restaure
|
||||
|
||||
# --- LES CIBLES QUE LES ROLES DECLARENT (2026-09-14) ----------------------------------
|
||||
#
|
||||
# `serveur_prometheus_cibles_supplementaires` existait depuis longtemps — une liste libre,
|
||||
|
|
|
|||
|
|
@ -28,6 +28,13 @@ DEUX GESTES, DEUX COTES :
|
|||
genome_colis.py etat <depot>
|
||||
Des deux cotes. Ce que cette copie porte, pour le confronter a la forge.
|
||||
|
||||
LES ETIQUETTES VOYAGENT AVEC LA BRANCHE (2026-10-07). Le colis ne portait que `main` : les
|
||||
releases n'arrivaient jamais sur la forge du site, qui fait pourtant autorite (D-81).
|
||||
`v2026.08.21` et `v2026.10.07` y manquaient, et il a fallu les pousser a la main. Le colis
|
||||
emporte desormais toutes les etiquettes du poste ; le runner les recoit et les pousse. Une
|
||||
etiquette qui existe deja SOUS UN AUTRE OBJET est refusee, des deux cotes : une release ne
|
||||
bouge pas.
|
||||
|
||||
`--ff-only` est le garde-fou. Si les deux histoires ont diverge, on s'arrete plutot que de
|
||||
fabriquer une fusion que personne n'a decidee — et le message de git nomme alors exactement
|
||||
ce qu'il faut reconcilier.
|
||||
|
|
@ -55,13 +62,18 @@ def _git(depot: Path, *args: str, env: dict | None = None) -> str:
|
|||
return (r.stdout or "").strip()
|
||||
|
||||
|
||||
def etiquettes(depot: Path) -> list[str]:
|
||||
return sorted(e for e in _git(depot, "tag", "-l").splitlines() if e)
|
||||
|
||||
|
||||
def emballer(depot: Path, colis: Path, branche: str) -> int:
|
||||
if not (depot / ".git").exists():
|
||||
raise SystemExit(f"{depot} n'est pas un depot git — rien a emballer.")
|
||||
colis.parent.mkdir(parents=True, exist_ok=True)
|
||||
_git(depot, "bundle", "create", str(colis), branche)
|
||||
_git(depot, "bundle", "create", str(colis), branche, "--tags")
|
||||
print(json.dumps({"depot": str(depot), "colis": str(colis),
|
||||
"tete": _git(depot, "log", "-1", "--format=%H")}))
|
||||
"tete": _git(depot, "log", "-1", "--format=%H"),
|
||||
"etiquettes": etiquettes(depot)}))
|
||||
return 0
|
||||
|
||||
|
||||
|
|
@ -101,6 +113,10 @@ def recevoir(depot: Path, colis: Path, pushurl: str, cle: str | None,
|
|||
f"refs/heads/{branche}:refs/remotes/colis/{branche}")
|
||||
_git(depot, "merge", "--ff-only", f"refs/remotes/colis/{branche}")
|
||||
apres = _git(depot, "log", "-1", "--format=%H")
|
||||
# Sans `--force` : une etiquette deja presente sous un autre objet fait echouer la
|
||||
# recuperation (« would clobber existing tag ») — c'est voulu.
|
||||
if any("refs/tags/" in l for l in _git(depot, "bundle", "list-heads", str(colis)).splitlines()):
|
||||
_git(depot, "fetch", str(colis), "refs/tags/*:refs/tags/*")
|
||||
|
||||
env = None
|
||||
if cle:
|
||||
|
|
@ -114,14 +130,15 @@ def recevoir(depot: Path, colis: Path, pushurl: str, cle: str | None,
|
|||
# seule l'ECRITURE s'authentifie, et seulement le temps de cette commande.
|
||||
r = subprocess.run(
|
||||
["git", "-C", str(depot), "-c", f"remote.origin.pushurl={pushurl}",
|
||||
"push", "origin", branche],
|
||||
"push", "origin", branche, "refs/tags/*:refs/tags/*"],
|
||||
capture_output=True, text=True, env=env)
|
||||
sortie = ((r.stdout or "") + (r.stderr or "")).strip()
|
||||
if r.returncode != 0:
|
||||
raise SystemExit(f"la poussee a echoue :\n {sortie}")
|
||||
|
||||
print(json.dumps({"avant": avant, "apres": apres,
|
||||
"pousse": f"{branche} -> {branche}" in sortie, "sortie": sortie[:400]}))
|
||||
"pousse": f"{branche} -> {branche}" in sortie,
|
||||
"etiquettes": etiquettes(depot), "sortie": sortie[:400]}))
|
||||
return 0
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -16,9 +16,11 @@ liste que `genome-pousser` — lue, pas recopiee) :
|
|||
demande de fusion acceptee, par exemple) : publier sans les integrer les ecraserait
|
||||
sur la forge du site ;
|
||||
2. signaler ce qui n'est pas committe — cela ne part pas ;
|
||||
3. pousser sur eregion ;
|
||||
puis `make genome-pousser`, puis `make genome-etat` : la forge du site doit porter la meme
|
||||
tete que le poste, sinon on le dit.
|
||||
3. pousser sur eregion, la branche ET les etiquettes (2026-10-07 : les releases n'allaient
|
||||
nulle part d'elles-memes ; une etiquette deplacee est refusee, une release ne bouge pas) ;
|
||||
puis `make genome-pousser`, qui porte aussi les etiquettes et verifie que la forge les a,
|
||||
puis `make genome-etat` : la forge du site doit porter la meme tete que le poste, sinon on
|
||||
le dit.
|
||||
|
||||
make publier tous les depots du genome
|
||||
make publier DEPOT=ops-chezlepro
|
||||
|
|
@ -81,7 +83,7 @@ def main() -> int:
|
|||
if sale:
|
||||
print(f" ~ {d['depot']:<17} {len(sale)} changement(s) NON committe(s) — ils ne partent pas")
|
||||
avance = git(depot, "rev-list", "--count", f"{distant}..HEAD").stdout.strip() or "?"
|
||||
r = git(depot, "push", "-q", "eregion", branche)
|
||||
r = git(depot, "push", "-q", "eregion", branche, "refs/tags/*:refs/tags/*")
|
||||
if r.returncode != 0:
|
||||
refus.append(d["depot"])
|
||||
print(f" !! {d['depot']:<17} push refuse : {r.stderr.strip()[:120]}")
|
||||
|
|
|
|||
112
scripts/tests/test_genome_colis.py
Normal file
112
scripts/tests/test_genome_colis.py
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
#!/usr/bin/env python3
|
||||
"""`genome_colis.py` : le colis porte la branche ET les releases jusqu'à la forge du site.
|
||||
|
||||
POURQUOI (2026-10-07). Le colis ne portait que `main`. Les étiquettes de release
|
||||
n'arrivaient donc jamais sur la forge du site, qui fait autorité (D-81) : `v2026.08.21` et
|
||||
`v2026.10.07` y manquaient, et il a fallu les pousser à la main.
|
||||
|
||||
Sur de vrais dépôts git, dans un répertoire jetable (aucun réseau) :
|
||||
- un poste, une forge nue, un runner cloné depuis la forge ;
|
||||
- le poste fait un commit et étiquette une release ; `emballer`, puis `recevoir` sur le
|
||||
runner, avec la forge pour `pushurl` ;
|
||||
- la forge porte la nouvelle tête ET l'étiquette ; un dépôt sans étiquette passe aussi ;
|
||||
- une étiquette déplacée sur le poste est REFUSÉE : une release ne bouge pas.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
RACINE = Path(__file__).resolve().parents[2]
|
||||
COLIS = RACINE / "scripts" / "genome_colis.py"
|
||||
ENV = {**os.environ, "GIT_AUTHOR_NAME": "t", "GIT_AUTHOR_EMAIL": "t@t", "GIT_COMMITTER_NAME": "t",
|
||||
"GIT_COMMITTER_EMAIL": "t@t", "GIT_CONFIG_GLOBAL": os.devnull, "GIT_CONFIG_NOSYSTEM": "1"}
|
||||
|
||||
ECHECS: list[str] = []
|
||||
|
||||
|
||||
def verifier(cond: bool, msg: str) -> None:
|
||||
print(("OK " if cond else "ECHEC ") + msg)
|
||||
if not cond:
|
||||
ECHECS.append(msg)
|
||||
|
||||
|
||||
def git(*args: str, cwd: Path | None = None) -> str:
|
||||
r = subprocess.run(["git", *args], cwd=cwd, capture_output=True, text=True, env=ENV)
|
||||
if r.returncode != 0:
|
||||
raise RuntimeError(f"git {' '.join(args)} : {r.stderr.strip()}")
|
||||
return r.stdout.strip()
|
||||
|
||||
|
||||
def colis(*args: str) -> subprocess.CompletedProcess:
|
||||
return subprocess.run([sys.executable, str(COLIS), *args], capture_output=True, text=True, env=ENV)
|
||||
|
||||
|
||||
def monter(d: Path) -> tuple[Path, Path, Path]:
|
||||
forge, poste, runner = d / "forge.git", d / "poste", d / "runner"
|
||||
git("init", "-q", "--bare", "-b", "main", str(forge))
|
||||
git("init", "-q", "-b", "main", str(poste))
|
||||
(poste / "a").write_text("1")
|
||||
git("add", "a", cwd=poste)
|
||||
git("commit", "-q", "-m", "un", cwd=poste)
|
||||
git("push", "-q", str(forge), "main", cwd=poste)
|
||||
git("clone", "-q", str(forge), str(runner))
|
||||
return forge, poste, runner
|
||||
|
||||
|
||||
def porter(poste: Path, runner: Path, forge: Path, d: Path) -> subprocess.CompletedProcess:
|
||||
paquet = d / "x.bundle"
|
||||
e = colis("emballer", str(poste), str(paquet))
|
||||
if e.returncode != 0:
|
||||
return e
|
||||
return colis("recevoir", str(runner), str(paquet), str(forge))
|
||||
|
||||
|
||||
def main() -> int:
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
d = Path(d)
|
||||
forge, poste, runner = monter(d)
|
||||
|
||||
# Un depot sans etiquette : rien ne change pour lui.
|
||||
(poste / "a").write_text("2")
|
||||
git("commit", "-q", "-am", "deux", cwd=poste)
|
||||
r = porter(poste, runner, forge, d)
|
||||
verifier(r.returncode == 0 and git("rev-parse", "main", cwd=forge) == git("rev-parse", "HEAD", cwd=poste),
|
||||
f"sans etiquette : la forge porte la tete du poste ({r.stderr.strip()[-160:]})")
|
||||
|
||||
# Une release annotee.
|
||||
(poste / "a").write_text("3")
|
||||
git("commit", "-q", "-am", "trois", cwd=poste)
|
||||
git("tag", "-a", "v1", "-m", "release v1", cwd=poste)
|
||||
r = porter(poste, runner, forge, d)
|
||||
recu = json.loads(r.stdout) if r.returncode == 0 else {}
|
||||
verifier(r.returncode == 0 and "v1" in git("tag", "-l", cwd=forge).split(),
|
||||
f"la release voyage jusqu'a la forge ({r.stderr.strip()[-160:]})")
|
||||
verifier(git("rev-parse", "v1", cwd=forge) == git("rev-parse", "v1", cwd=poste),
|
||||
"la forge porte le MEME objet d'etiquette que le poste (annotee, pas recreee)")
|
||||
verifier(recu.get("etiquettes") == ["v1"], f"le runner dit les etiquettes qu'il porte ({recu.get('etiquettes')})")
|
||||
e = json.loads(colis("emballer", str(poste), str(d / "y.bundle")).stdout)
|
||||
verifier(e.get("etiquettes") == ["v1"], "l'emballage dit les etiquettes du poste")
|
||||
|
||||
# Une release deplacee sur le poste : refusee, la forge garde l'ancienne.
|
||||
ancienne = git("rev-parse", "v1", cwd=forge)
|
||||
(poste / "a").write_text("4")
|
||||
git("commit", "-q", "-am", "quatre", cwd=poste)
|
||||
git("tag", "-f", "-a", "v1", "-m", "v1 deplacee", cwd=poste)
|
||||
r = porter(poste, runner, forge, d)
|
||||
verifier(r.returncode != 0 and git("rev-parse", "v1", cwd=forge) == ancienne,
|
||||
f"une etiquette deplacee est refusee, la forge garde la release ({r.stderr.strip()[-120:]})")
|
||||
|
||||
if ECHECS:
|
||||
print(f"\n{len(ECHECS)} echec(s).")
|
||||
return 1
|
||||
print("\nLe colis porte la branche et les releases ; une release ne bouge pas.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
|
|
@ -221,6 +221,21 @@ if (( ! avec_c )); then cat > "{log}/section.sql"; fi
|
|||
f"--remplacer remet l'etat d'avant, a l'identique ({r.stdout.strip() or r.stderr.strip()})")
|
||||
verifier(any((d / "mis-de-cote").rglob("x")), "l'etat ecrase a ete mis de cote")
|
||||
|
||||
# LA COPIE A FROID (2026-10-07) : l'instantane porte la copie posee dans la
|
||||
# preparation ; `--depuis` la remet a sa place vivante, qui porte un autre nom.
|
||||
copie = fixt / "avant01" / str(staging).lstrip("/") / "prometheus" / "metrics2" / ("01" + "B" * 24)
|
||||
copie.mkdir(parents=True)
|
||||
(copie / "meta.json").write_text("{}")
|
||||
vivante = d / "prometheus-vivant"
|
||||
(vivante / "wal").mkdir(parents=True)
|
||||
(vivante / "wal" / "00000000").write_text("neuf")
|
||||
r = lancer("fichiers", "--depuis", str(staging / "prometheus" / "metrics2"), "--remplacer", str(vivante))
|
||||
verifier(r.returncode == 0 and (vivante / ("01" + "B" * 24) / "meta.json").exists()
|
||||
and not (vivante / "wal").exists(),
|
||||
f"--depuis remet la copie a froid a sa place vivante ({r.stdout.strip() or r.stderr.strip()})")
|
||||
r = lancer("fichiers", "--depuis", str(staging), str(d / "a"), str(d / "b"))
|
||||
verifier(r.returncode != 0, "--depuis refuse plusieurs destinations")
|
||||
|
||||
# LES TEMOINS (2026-10-07) : l'instantane etiquete au rasage, pas le candidat.
|
||||
seul_courriel = rendre("setops-restaurer-courriel", [vmail_jeu])
|
||||
r = lancer("temoins", outil=seul_courriel)
|
||||
|
|
@ -327,10 +342,87 @@ exit 0
|
|||
"un depot ordinaire garde lui aussi l'instantane d'avant rasage (c'est lui qui le chassait)")
|
||||
|
||||
|
||||
# --- 4. La copie a froid (2026-10-07) -----------------------------------------------------
|
||||
|
||||
def copie_a_froid() -> None:
|
||||
"""Le service est arrete le temps de copier, et relance QUOI QU'IL ARRIVE ; les blocs
|
||||
immuables sont lies, le reste copie ; un service inactif n'est ni arrete ni relance."""
|
||||
outil = RACINE / "roles/client_backup/files/setops-copie-a-froid.sh"
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
d = Path(d)
|
||||
bin_, journal = d / "bin", d / "systemctl.log"
|
||||
bin_.mkdir()
|
||||
executable(bin_ / "systemctl", f"""#!/bin/bash
|
||||
echo "$*" >> "{journal}"
|
||||
[[ "$1" == is-active ]] && exit "${{SETOPS_TEST_ACTIF:-0}}"
|
||||
exit 0
|
||||
""")
|
||||
# `cp` qui echoue sur le WAL quand on le demande : une copie ratee en plein milieu.
|
||||
executable(bin_ / "cp", '#!/bin/bash\n[[ -n "${SETOPS_TEST_CP_ECHOUE:-}" && "$*" == *wal* ]] && exit 1\nexec /bin/cp "$@"\n')
|
||||
source, dest = d / "metrics2", d / "prep" / "metrics2"
|
||||
bloc = source / ("01" + "C" * 24)
|
||||
bloc.mkdir(parents=True)
|
||||
(bloc / "index").write_text("bloc")
|
||||
(source / "wal").mkdir()
|
||||
(source / "wal" / "00000001").write_text("wal")
|
||||
env = {**os.environ, "PATH": f"{bin_}:{os.environ['PATH']}"}
|
||||
|
||||
def lancer(**extra: str) -> subprocess.CompletedProcess:
|
||||
journal.unlink(missing_ok=True)
|
||||
return subprocess.run(["bash", str(outil), "prometheus", str(source), str(dest), "--blocs-immuables"],
|
||||
env={**env, **extra}, capture_output=True, text=True)
|
||||
|
||||
r = lancer()
|
||||
appels = journal.read_text().split("\n") if journal.exists() else []
|
||||
verifier(r.returncode == 0 and appels[:3] == ["is-active --quiet prometheus", "stop prometheus", "start prometheus"],
|
||||
f"arrete, copie, relance ({appels[:3]}, {r.stderr.strip()[-120:]})")
|
||||
verifier((dest / bloc.name / "index").stat().st_ino == (bloc / "index").stat().st_ino,
|
||||
"un bloc immuable est lie (meme inode), pas recopie")
|
||||
verifier((dest / "wal" / "00000001").stat().st_ino != (source / "wal" / "00000001").stat().st_ino
|
||||
and (dest / "wal" / "00000001").read_text() == "wal",
|
||||
"le WAL est vraiment copie (un autre inode)")
|
||||
verifier("arrete" in r.stdout and " ms" in r.stdout, f"la duree d'arret est dite ({r.stdout.strip()})")
|
||||
|
||||
(dest / "temoin").write_text("copie precedente")
|
||||
r = lancer(SETOPS_TEST_CP_ECHOUE="1")
|
||||
appels = journal.read_text().split("\n") if journal.exists() else []
|
||||
verifier(r.returncode != 0 and "start prometheus" in appels,
|
||||
f"une copie ratee : le service est QUAND MEME relance ({appels})")
|
||||
verifier((dest / "temoin").exists(), "une copie ratee ne remplace pas la copie precedente")
|
||||
|
||||
r = lancer(SETOPS_TEST_ACTIF="3")
|
||||
appels = journal.read_text().split("\n") if journal.exists() else []
|
||||
verifier(r.returncode == 0 and "stop prometheus" not in appels and "start prometheus" not in appels,
|
||||
"un service inactif n'est ni arrete ni relance")
|
||||
|
||||
# LOKI (2026-10-07) : les morceaux sont lies, `chunks/index/` copie. Il loge la base
|
||||
# du compacteur (`delete_requests.gz`), qui se reecrit : liee, la sauvegarde
|
||||
# changerait avec elle.
|
||||
loki, prep = d / "loki", d / "prep" / "donnees"
|
||||
(loki / "chunks" / "fake").mkdir(parents=True)
|
||||
(loki / "chunks" / "fake" / "MWEw").write_text("morceau")
|
||||
(loki / "chunks" / "index" / "delete_requests").mkdir(parents=True)
|
||||
(loki / "chunks" / "index" / "delete_requests" / "delete_requests.gz").write_text("v1")
|
||||
(loki / "wal").mkdir()
|
||||
(loki / "wal" / "00000001").write_text("wal")
|
||||
r = subprocess.run(["bash", str(outil), "loki", str(loki), str(prep), "--lier", "chunks",
|
||||
"--copier", "chunks/index"], env=env, capture_output=True, text=True)
|
||||
verifier(r.returncode == 0 and (prep / "chunks" / "fake" / "MWEw").stat().st_ino
|
||||
== (loki / "chunks" / "fake" / "MWEw").stat().st_ino,
|
||||
f"Loki : un morceau est lie (meme inode) ({r.stderr.strip()[-120:]})")
|
||||
base = loki / "chunks" / "index" / "delete_requests" / "delete_requests.gz"
|
||||
copie = prep / "chunks" / "index" / "delete_requests" / "delete_requests.gz"
|
||||
verifier(copie.exists() and copie.stat().st_ino != base.stat().st_ino,
|
||||
"Loki : chunks/index est vraiment copie (un autre inode)")
|
||||
base.write_text("v2") # le compacteur reecrit sa base sur place
|
||||
verifier(copie.read_text() == "v1", "une reecriture apres la copie ne change pas la sauvegarde")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
couverture()
|
||||
outil()
|
||||
sauvegarde()
|
||||
copie_a_froid()
|
||||
if ECHECS:
|
||||
print(f"\n{len(ECHECS)} echec(s).")
|
||||
return 1
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ from __future__ import annotations
|
|||
import base64
|
||||
import contextlib
|
||||
import io
|
||||
import json
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
|
|
@ -270,11 +271,78 @@ def icinga() -> None:
|
|||
verifier(rc == 1 and "IDENTITE ca.key" in out, "une cle d'AC d'Icinga changee est un ECART")
|
||||
|
||||
|
||||
def bloc(racine: Path, nom: str, debut: int, fin: int) -> None:
|
||||
ecrire(racine / nom / "meta.json", json.dumps({"minTime": debut, "maxTime": fin}))
|
||||
ecrire(racine / nom / "chunks" / "000001", nom)
|
||||
|
||||
|
||||
def prometheus_loki() -> None:
|
||||
"""Prometheus fusionne ses blocs et la retention en retire : on juge la COUVERTURE.
|
||||
Loki : aucun morceau d'avant = non restaure ; une perte partielle = retention."""
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
d = Path(d)
|
||||
staging = "/var/backups/setops/prometheus"
|
||||
avant = d / "avant" / staging.lstrip("/") / "metrics2"
|
||||
vivant = d / "vivant"
|
||||
U = "01" + "A" * 24
|
||||
bloc(avant, U[:-1] + "1", 1000, 2000)
|
||||
bloc(avant, U[:-1] + "2", 2000, 3000) # le dernier bloc d'avant commence a 2000
|
||||
argv = ["--avant", str(d / "avant"), "--instantane", "x", "--jeu", f"prometheus={staging}",
|
||||
"--vivant", f"prometheus={vivant}"]
|
||||
|
||||
bloc(vivant, U[:-1] + "9", 1000, 3000) # restaure, puis fusionne en un bloc
|
||||
bloc(vivant, U[:-1] + "8", 3000, 4000)
|
||||
rc, out = lancer(argv)
|
||||
verifier(rc == 0, f"blocs restaures puis fusionnes : sans ecart ({out.strip()[-140:]})")
|
||||
|
||||
shutil.rmtree(vivant)
|
||||
bloc(vivant, U[:-1] + "7", 9000, 9500) # une base nee apres la reconstruction
|
||||
rc, out = lancer(argv)
|
||||
verifier(rc == 1 and "non restaurees" in out, "une base de Prometheus non restauree est un ECART")
|
||||
|
||||
shutil.rmtree(vivant)
|
||||
bloc(vivant, U[:-1] + "6", 2000, 4000) # la retention a retire le plus ancien
|
||||
rc, out = lancer(argv)
|
||||
verifier(rc == 0 and "retention" in out, "la retention qui retire le plus ancien bloc : dite, sans ecart")
|
||||
|
||||
# Une base de moins de deux heures : pas de bloc, le WAL fait le repere.
|
||||
shutil.rmtree(avant)
|
||||
shutil.rmtree(vivant)
|
||||
ecrire(avant / "wal" / "00000003", "w")
|
||||
ecrire(vivant / "wal" / "00000003", "w")
|
||||
ecrire(vivant / "wal" / "00000004", "w")
|
||||
rc, out = lancer(argv)
|
||||
verifier(rc == 0, "sans bloc, le WAL d'avant retrouve dans la base vivante : sans ecart")
|
||||
shutil.rmtree(vivant)
|
||||
ecrire(vivant / "wal" / "00000000", "w")
|
||||
rc, out = lancer(argv)
|
||||
verifier(rc == 1 and "WAL d'avant" in out, "sans bloc, un WAL neuf a la place de celui d'avant : ECART")
|
||||
|
||||
staging = "/var/backups/setops/loki"
|
||||
avant = d / "avant" / staging.lstrip("/") / "donnees"
|
||||
vivant = d / "loki"
|
||||
for racine in (avant, vivant):
|
||||
ecrire(racine / "chunks" / "fake" / "a", "1")
|
||||
ecrire(racine / "chunks" / "fake" / "b", "2")
|
||||
ecrire(vivant / "chunks" / "fake" / "c", "3")
|
||||
argv = ["--avant", str(d / "avant"), "--instantane", "x", "--jeu", f"loki={staging}",
|
||||
"--vivant", f"loki={vivant}"]
|
||||
rc, out = lancer(argv)
|
||||
verifier(rc == 0 and "1 morceau(x) nouveau(x)" in out, "Loki restaure, des journaux arrives depuis : sans ecart")
|
||||
(vivant / "chunks" / "fake" / "a").unlink()
|
||||
rc, out = lancer(argv)
|
||||
verifier(rc == 0 and "retire(s)" in out, "un morceau d'avant retire (retention) : dit, sans ecart")
|
||||
(vivant / "chunks" / "fake" / "b").unlink()
|
||||
rc, out = lancer(argv)
|
||||
verifier(rc == 1 and "journaux non restaures" in out, "aucun morceau d'avant : Loki non restaure, ECART")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
fichiers()
|
||||
annuaire()
|
||||
postgresql()
|
||||
icinga()
|
||||
prometheus_loki()
|
||||
if ECHECS:
|
||||
print(f"\n{len(ECHECS)} echec(s).")
|
||||
return 1
|
||||
|
|
|
|||
Loading…
Reference in a new issue