prometheus et loki : copie a froid, restauration et temoins
Les metriques et les journaux repartaient de zero a chaque reconstruction. setops-copie-a-froid arrete le service le temps de copier et le relance quoi qu il arrive ; les roles remettent la copie avant de demarrer ; les temoins jugent la couverture, pas les fichiers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
6141bc4b2f
commit
4660a8892a
12 changed files with 491 additions and 5 deletions
51
CHANGELOG.md
51
CHANGELOG.md
|
|
@ -1,5 +1,56 @@
|
||||||
# CHANGELOG — Set-OPS
|
# CHANGELOG — Set-OPS
|
||||||
|
|
||||||
|
## 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**.
|
||||||
|
|
||||||
|
**Pas encore éprouvé** : rien n'est déployé. Le trou de collecte réel (arrêt et rejeu du
|
||||||
|
WAL) se mesurera au premier dépôt.
|
||||||
|
|
||||||
|
**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
|
## 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`,
|
**94 preuves.** L'exploitant a déclaré obsolète l'ancien gabarit, `99998`. Le neuf (`9006`,
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,29 @@ client_backup_catalogue:
|
||||||
# PAS DE `serveur_web_frontal` (retire le 2026-09-30) : le frontal RELAIE, il ne sert
|
# 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.
|
# 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.
|
# `/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
|
||||||
|
chemins: ["{{ client_backup_staging }}/loki"]
|
||||||
serveur_icinga:
|
serveur_icinga:
|
||||||
# L'AC D'ICINGA FAIT L'IDENTITE DE SA BASE (2026-10-07). Icinga 2 derive l'environnement
|
# L'AC D'ICINGA FAIT L'IDENTITE DE SA BASE (2026-10-07). Icinga 2 derive l'environnement
|
||||||
# d'Icinga DB de la cle publique de son AC, et chaque identifiant d'hote, de service et
|
# d'Icinga DB de la cle publique de son AC, et chaque identifiant d'hote, de service et
|
||||||
|
|
|
||||||
62
roles/client_backup/files/setops-copie-a-froid.sh
Normal file
62
roles/client_backup/files/setops-copie-a-froid.sh
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
#!/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]
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
service="${1:?service}"
|
||||||
|
source="${2:?source}"
|
||||||
|
dest="${3:?destination}"
|
||||||
|
blocs="${4:-}"
|
||||||
|
|
||||||
|
[[ -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
|
||||||
|
|
||||||
|
if [[ "${blocs}" == "--blocs-immuables" ]]; then
|
||||||
|
mkdir -p "${tmp}"
|
||||||
|
for e in "${source}"/* "${source}"/.[!.]*; do
|
||||||
|
[[ -e "${e}" ]] || continue
|
||||||
|
n="$(basename "${e}")"
|
||||||
|
if [[ -d "${e}" && ${#n} -eq 26 && "${n}" == 01* ]]; then
|
||||||
|
cp -al "${e}" "${tmp}/${n}" 2>/dev/null || cp -a "${e}" "${tmp}/${n}"
|
||||||
|
else
|
||||||
|
cp -a "${e}" "${tmp}/${n}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
else
|
||||||
|
cp -a "${source}" "${tmp}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
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,
|
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
|
`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
|
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.
|
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.
|
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 base64
|
||||||
import collections
|
import collections
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import subprocess
|
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)'}")
|
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 ----------------------------------------------------------------------
|
# --- L'annuaire ----------------------------------------------------------------------
|
||||||
|
|
||||||
def entrees_ldif(texte: str) -> dict[str, dict[str, list[str]]]:
|
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("--heure", default="")
|
||||||
ap.add_argument("--hote", default="")
|
ap.add_argument("--hote", default="")
|
||||||
ap.add_argument("--jeu", action="append", default=[], help="NOM=CHEMIN[:CHEMIN...]")
|
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", default="", help="chemin du LDIF dans l'instantané")
|
||||||
ap.add_argument("--ldif-vivant", type=Path, default=None)
|
ap.add_argument("--ldif-vivant", type=Path, default=None)
|
||||||
ap.add_argument("--dump", default="", help="chemin du pg_dumpall dans l'instantané")
|
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.")
|
print(" SANS OBJET : ce noeud ne detient aucun etat.")
|
||||||
return 0
|
return 0
|
||||||
ecart = False
|
ecart = False
|
||||||
|
vivants = dict(v.partition("=")[::2] for v in a.vivant)
|
||||||
for spec in a.jeu:
|
for spec in a.jeu:
|
||||||
nom, _, chemins = spec.partition("=")
|
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)"
|
r, unite = jeu_annuaire(a.avant / a.ldif.lstrip("/"), a.ldif_vivant or Path("/nonexistent")), "entree(s)"
|
||||||
elif nom == "postgresql":
|
elif nom == "postgresql":
|
||||||
r, unite = jeu_postgresql(a.avant / a.dump.lstrip("/"), interroger), "table(s)"
|
r, unite = jeu_postgresql(a.avant / a.dump.lstrip("/"), interroger), "table(s)"
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,17 @@
|
||||||
group: root
|
group: root
|
||||||
mode: "0700"
|
mode: "0700"
|
||||||
|
|
||||||
|
# LA COPIE A FROID (2026-10-07) : les jeux qui s'ecrivent en continu (Prometheus, Loki)
|
||||||
|
# arretent leur service le temps de copier. L'outil est deploye avec le script qui l'appelle.
|
||||||
|
- name: Déployer l'outil de copie à froid
|
||||||
|
when: client_backup_jobs | length > 0
|
||||||
|
ansible.builtin.copy:
|
||||||
|
src: setops-copie-a-froid.sh
|
||||||
|
dest: /usr/local/sbin/setops-copie-a-froid
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: "0700"
|
||||||
|
|
||||||
- name: Déployer l'unité et le timer systemd
|
- name: Déployer l'unité et le timer systemd
|
||||||
when: client_backup_jobs | length > 0
|
when: client_backup_jobs | length > 0
|
||||||
ansible.builtin.template:
|
ansible.builtin.template:
|
||||||
|
|
@ -116,6 +127,7 @@
|
||||||
state: absent
|
state: absent
|
||||||
loop:
|
loop:
|
||||||
- /usr/local/sbin/setops-sauvegarder.sh
|
- /usr/local/sbin/setops-sauvegarder.sh
|
||||||
|
- /usr/local/sbin/setops-copie-a-froid
|
||||||
- /etc/systemd/system/setops-sauvegarde.service
|
- /etc/systemd/system/setops-sauvegarde.service
|
||||||
- /etc/systemd/system/setops-sauvegarde.timer
|
- /etc/systemd/system/setops-sauvegarde.timer
|
||||||
- /etc/systemd/system/setops-sauvegarde-avant-raser.service
|
- /etc/systemd/system/setops-sauvegarde-avant-raser.service
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,8 @@
|
||||||
# choisir [--instantane ID] (pour Ansible) JSON de l'instantane candidat
|
# choisir [--instantane ID] (pour Ansible) JSON de l'instantane candidat
|
||||||
# fichiers [opts] CHEMIN... remettre des repertoires (rsync --delete)
|
# fichiers [opts] CHEMIN... remettre des repertoires (rsync --delete)
|
||||||
# --proprietaire U:G --remplacer --vers DOSSIER (repetition, rien n'est touche)
|
# --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)
|
# annuaire [opts] BASE_DN remplacer la base LDAP (slapadd)
|
||||||
# --essai (slapadd -u a blanc, rien n'est touche)
|
# --essai (slapadd -u a blanc, rien n'est touche)
|
||||||
# base [opts] NOM rejouer une base depuis le pg_dumpall
|
# 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.
|
# 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 %} )
|
declare -A CHEMINS_JEU=({% for j in client_backup_jobs %} ["{{ j.nom }}"]="{{ j.chemins | join(':') }}"{% endfor %} )
|
||||||
ETIQUETTE_AVANT_RASER="{{ client_backup_etiquette_avant_raser }}"
|
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; }
|
dire() { printf '%s\n' "$*" >&2; }
|
||||||
mourir() { dire "setops-restaurer: $*"; exit 1; }
|
mourir() { dire "setops-restaurer: $*"; exit 1; }
|
||||||
|
|
@ -154,22 +159,28 @@ vide() { # vrai si le chemin est absent, ou un repertoire sans contenu
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd_fichiers() {
|
cmd_fichiers() {
|
||||||
local proprio="" remplacer=0 vers="" chemins=() src dest n
|
local proprio="" remplacer=0 vers="" depuis="" chemins=() src dest n
|
||||||
while (( $# )); do
|
while (( $# )); do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
--proprietaire) proprio="$2"; shift 2 ;;
|
--proprietaire) proprio="$2"; shift 2 ;;
|
||||||
--remplacer) remplacer=1; shift ;;
|
--remplacer) remplacer=1; shift ;;
|
||||||
--vers) vers="$2"; shift 2 ;;
|
--vers) vers="$2"; shift 2 ;;
|
||||||
|
--depuis) depuis="$2"; shift 2 ;;
|
||||||
--instantane) INSTANTANE="$2"; shift 2 ;;
|
--instantane) INSTANTANE="$2"; shift 2 ;;
|
||||||
*) chemins+=("$1"); shift ;;
|
*) chemins+=("$1"); shift ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
[[ -n "${chemins[*]:-}" ]] || mourir "fichiers : aucun chemin"
|
[[ -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
|
resoudre_instantane
|
||||||
if [[ -z "${INSTANTANE}" ]]; then echo "RIEN : aucune incarnation precedente."; return 0; fi
|
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
|
for c in "${chemins[@]}"; do
|
||||||
src="${TMP}${c}"
|
src="${TMP}${depuis:-${c}}"
|
||||||
dest="${vers}${c}"
|
dest="${vers}${c}"
|
||||||
if [[ ! -e "${src}" ]]; then echo "ABSENT de ${INSTANTANE} : ${c}"; continue; fi
|
if [[ ! -e "${src}" ]]; then echo "ABSENT de ${INSTANTANE} : ${c}"; continue; fi
|
||||||
if ! vide "${dest}"; then
|
if ! vide "${dest}"; then
|
||||||
|
|
@ -358,6 +369,7 @@ cmd_temoins() {
|
||||||
IFS=: read -r -a morceaux <<< "${CHEMINS_JEU[${j}]}"
|
IFS=: read -r -a morceaux <<< "${CHEMINS_JEU[${j}]}"
|
||||||
chemins+=("${morceaux[@]}")
|
chemins+=("${morceaux[@]}")
|
||||||
args+=(--jeu "${j}=${CHEMINS_JEU[${j}]}")
|
args+=(--jeu "${j}=${CHEMINS_JEU[${j}]}")
|
||||||
|
if [[ -n "${VIVANT_JEU[${j}]:-}" ]]; then args+=(--vivant "${j}=${VIVANT_JEU[${j}]}"); fi
|
||||||
done
|
done
|
||||||
extraire "${chemins[@]}"
|
extraire "${chemins[@]}"
|
||||||
if [[ -s "${TMP}${LDIF}" ]]; then
|
if [[ -s "${TMP}${LDIF}" ]]; then
|
||||||
|
|
|
||||||
|
|
@ -21,3 +21,5 @@ client_backup_groupes_etat:
|
||||||
- serveur_rspamd
|
- serveur_rspamd
|
||||||
- serveur_web_dorsal
|
- serveur_web_dorsal
|
||||||
- serveur_icinga
|
- serveur_icinga
|
||||||
|
- serveur_prometheus
|
||||||
|
- serveur_loki
|
||||||
|
|
|
||||||
|
|
@ -195,6 +195,56 @@
|
||||||
group: loki
|
group: loki
|
||||||
mode: "0750"
|
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 ---
|
# --- TLS : synchroniser le cert step_ca AVANT d'activer http_tls_config ---
|
||||||
- name: TLS — repertoire des certificats (loki)
|
- name: TLS — repertoire des certificats (loki)
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,9 @@ serveur_prometheus_paquets:
|
||||||
|
|
||||||
serveur_prometheus_service: "prometheus"
|
serveur_prometheus_service: "prometheus"
|
||||||
serveur_prometheus_config: "/etc/prometheus/prometheus.yml"
|
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_intervalle: "15s"
|
||||||
serveur_prometheus_retention: "15d"
|
serveur_prometheus_retention: "15d"
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,58 @@
|
||||||
update_cache: true
|
update_cache: true
|
||||||
cache_valid_time: 3600
|
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) ----------------------------------
|
# --- LES CIBLES QUE LES ROLES DECLARENT (2026-09-14) ----------------------------------
|
||||||
#
|
#
|
||||||
# `serveur_prometheus_cibles_supplementaires` existait depuis longtemps — une liste libre,
|
# `serveur_prometheus_cibles_supplementaires` existait depuis longtemps — une liste libre,
|
||||||
|
|
|
||||||
|
|
@ -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()})")
|
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")
|
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.
|
# LES TEMOINS (2026-10-07) : l'instantane etiquete au rasage, pas le candidat.
|
||||||
seul_courriel = rendre("setops-restaurer-courriel", [vmail_jeu])
|
seul_courriel = rendre("setops-restaurer-courriel", [vmail_jeu])
|
||||||
r = lancer("temoins", outil=seul_courriel)
|
r = lancer("temoins", outil=seul_courriel)
|
||||||
|
|
@ -327,10 +342,65 @@ exit 0
|
||||||
"un depot ordinaire garde lui aussi l'instantane d'avant rasage (c'est lui qui le chassait)")
|
"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")
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
couverture()
|
couverture()
|
||||||
outil()
|
outil()
|
||||||
sauvegarde()
|
sauvegarde()
|
||||||
|
copie_a_froid()
|
||||||
if ECHECS:
|
if ECHECS:
|
||||||
print(f"\n{len(ECHECS)} echec(s).")
|
print(f"\n{len(ECHECS)} echec(s).")
|
||||||
return 1
|
return 1
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ from __future__ import annotations
|
||||||
import base64
|
import base64
|
||||||
import contextlib
|
import contextlib
|
||||||
import io
|
import io
|
||||||
|
import json
|
||||||
import shutil
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
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")
|
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:
|
def main() -> int:
|
||||||
fichiers()
|
fichiers()
|
||||||
annuaire()
|
annuaire()
|
||||||
postgresql()
|
postgresql()
|
||||||
icinga()
|
icinga()
|
||||||
|
prometheus_loki()
|
||||||
if ECHECS:
|
if ECHECS:
|
||||||
print(f"\n{len(ECHECS)} echec(s).")
|
print(f"\n{len(ECHECS)} echec(s).")
|
||||||
return 1
|
return 1
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue