sondes de sauvegarde : juger le plus recent instantane ; temoin Loki hors index
restic snapshots --latest 1 rend un instantane par groupe (machine et chemins) : la sonde gardait le premier, l'ancien, depuis qu'un jeu a ete ajoute a site-mon-01 (EN RETARD a tort, PERIME promis a 50 h). Meme motif cote serveur_backup. Le temoin de Loki ne compte plus l'index compacte. Journal (114), (115). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
9ca9a5ffe1
commit
0f7c89fbb5
7 changed files with 173 additions and 5 deletions
55
CHANGELOG.md
55
CHANGELOG.md
|
|
@ -1,5 +1,60 @@
|
|||
# CHANGELOG — Set-OPS
|
||||
|
||||
## 2026-10-08 (115) — La sonde du dépôt jugeait le premier groupe d'instantanés, pas le plus récent
|
||||
|
||||
**94 preuves.** Question de l'exploitant : « on a aussi les sondes pour vérifier que les
|
||||
sauvegardes sont OK ? » Oui : le contrôle du dépôt (toutes les 4 h : existe, récent, non vide,
|
||||
lu dans le dépôt distant) et le contrôle de restauration (chaque semaine : relecture d'une
|
||||
part des données, vraie restauration vérifiée), poussés à Icinga, dont le silence alerte.
|
||||
Leur état, lu dans Icinga DB des trois écosystèmes : tout OK chez les locataires (dont
|
||||
`obs-01`), sauf `web-dorsal-01` (rien à sauvegarder, aucune application déployée). Au site :
|
||||
**`site-mon-01` « EN RETARD : dernier instantané il y a 26 h »**, alors que la sauvegarde de
|
||||
la nuit était passée à 02:32.
|
||||
|
||||
### Cause : le défaut venait de mon chantier de la veille
|
||||
|
||||
`restic snapshots --latest 1` rend le dernier instantané de **chaque groupe** (machine ET liste
|
||||
de chemins). Ajouter `prometheus`, `loki` et `icinga` à `site-mon-01` a changé sa liste : deux
|
||||
groupes. La sonde gardait `d[0]`, l'ancien, figé au 2026-10-07 02:41. Il n'aurait fait que
|
||||
vieillir : **« PÉRIMÉ » (critique) à 50 h**, le 2026-10-09 au matin, sur une sauvegarde saine,
|
||||
et de même sur toute machine qui reçoit un nouveau jeu. Même motif dans
|
||||
`serveur_backup/verifier-sauvegardes.sh.j2`.
|
||||
|
||||
**Fait** : les deux sondes retiennent le plus récent de tous les instantanés rendus.
|
||||
**Éprouvé** : `test_verifier_depot.py` (dans `make test`), la sonde rendue depuis son gabarit,
|
||||
`restic` et `curl` doublés : deux groupes, l'ancien (30 h) en premier, verdict OK (1 h) ; un
|
||||
seul groupe vieux de 30 h, toujours EN RETARD. Témoin : l'ancien gabarit rend « EN RETARD :
|
||||
30 h », le symptôme du site. `make verifier` conforme, **94/94**.
|
||||
|
||||
## 2026-10-08 (114) — Les métriques et les journaux traversent la reconstruction
|
||||
|
||||
**94 preuves.** Technolibre rasée et reconstruite sur `2dce6ea` (arrêt à `valider`, entrée
|
||||
(113) ; recette repassée sur `9ca9a5f`, reprise `DEPUIS=parefeu`). Journal :
|
||||
`OPS-Technolibre/logs/reconstruction-20261008-131846.log`, puis `…-162819.log`.
|
||||
|
||||
### La restauration de Prometheus et de Loki, vécue
|
||||
|
||||
**9 jeux** restaurés depuis l'instantané `avant-raser` de 13:18:50, chacun encore au dépôt :
|
||||
dont `prometheus` et `loki` (obs-01) et `icinga` (mon-01). Pare-feu Proxmox 13/13, 0 critique.
|
||||
Témoins : **aucun écart**. Prometheus : un bloc vivant couvre le dernier bloc d'avant ; AC
|
||||
d'Icinga identique ; historique d'Icinga, 803 lignes d'avant, 0 retirée.
|
||||
|
||||
**Demandé aux bases elles-mêmes**, sur 12:00–13:18 (avant le rasage) :
|
||||
- Prometheus : un point toutes les 10 minutes, 15 cibles à chacun ;
|
||||
- Loki : neuf tranches de 10 minutes, environ 1 000 lignes chacune, jusqu'à 13:18.
|
||||
|
||||
### Le témoin de Loki comptait l'index parmi les morceaux
|
||||
|
||||
Il disait « 3 morceaux d'avant retirés (rétention ?) ». La base a moins de deux jours, ce ne
|
||||
pouvait pas être la rétention. Liste récursive de l'instantané contre la base vivante (un
|
||||
premier `restic ls` sans `--recursive` ne listait que le premier niveau) : les trois sont des
|
||||
fichiers d'**index** (`chunks/index/index_20734/…tsdb.gz`, dont une sortie du compacteur), que
|
||||
le compacteur fusionne et remplace. Aucun journal n'était perdu.
|
||||
|
||||
**Fait** : le témoin de Loki ne compare que les morceaux, hors `chunks/index/` (que la copie à
|
||||
froid traite déjà à part). Test : un index compacté ne fait ni écart ni retrait ; témoin :
|
||||
avec l'ancienne règle, il échoue. `make verifier` conforme, **94/94**.
|
||||
|
||||
## 2026-10-08 (113) — La recette jugeait Prometheus 18 secondes après sa relance
|
||||
|
||||
**94 preuves.** La reconstruction de Technolibre (`2dce6ea`) s'est arrêtée à `valider` :
|
||||
|
|
|
|||
1
Makefile
1
Makefile
|
|
@ -319,6 +319,7 @@ test: ## Lance les tests unitaires (derivation de nomenclature et d'inventaire)
|
|||
python3 scripts/tests/test_raser_resultat.py
|
||||
python3 scripts/tests/test_temoins.py
|
||||
python3 scripts/tests/test_genome_colis.py
|
||||
python3 scripts/tests/test_verifier_depot.py
|
||||
python3 scripts/tests/test_adressage_derive.py
|
||||
python3 scripts/tests/test_gui_intrants.py
|
||||
python3 scripts/tests/test_runbooks.py
|
||||
|
|
|
|||
|
|
@ -233,8 +233,13 @@ def jeu_loki(avant: Path, vivant: Path) -> Rapport:
|
|||
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"))
|
||||
# LES MORCEAUX, PAS L'INDEX (2026-10-08). `chunks/index/` loge l'index TSDB et la base du
|
||||
# compacteur : il les fusionne et les remplace. A la reconstruction de Technolibre, trois
|
||||
# de ces fichiers « d'avant » manquaient, et le temoin les disait retires « (retention ?) »
|
||||
# — aucun journal ne l'etait (Loki rendait ceux d'avant le rasage, minute par minute).
|
||||
morceaux = lambda racine: {rel for rel in arbre(racine / "chunks") if not rel.startswith("index/")} # noqa: E731
|
||||
a = morceaux(avant)
|
||||
v = morceaux(vivant)
|
||||
r.compte = len(a)
|
||||
if a and not (a & v):
|
||||
r.ecarts.append(f"aucun des {len(a)} morceau(x) de journal d'avant ne subsiste : journaux non restaures")
|
||||
|
|
|
|||
|
|
@ -56,9 +56,17 @@ rapporter() { # $1=code $2=texte
|
|||
|
||||
maintenant=$(date +%s)
|
||||
|
||||
# LE PLUS RECENT DE TOUS LES GROUPES, PAS LE PREMIER (2026-10-08). `--latest 1` rend le
|
||||
# dernier instantane de CHAQUE groupe — machine ET liste de chemins. Ajouter un jeu change la
|
||||
# liste : deux groupes, et `d[0]` etait l'ancien, fige. Sur site-mon-01, des le lendemain de
|
||||
# l'arrivee de prometheus, loki et icinga : « EN RETARD : 26 h » sur une sauvegarde de 2 h 30,
|
||||
# et « PERIME » promis a 50 h. On prend donc le plus recent de tous.
|
||||
json=$(restic snapshots --latest 1 --json 2>/dev/null)
|
||||
horodatage=$(printf '%s' "${json}" | python3 -c \
|
||||
'import sys,json;d=json.load(sys.stdin);print(d[0]["time"] if d else "")' 2>/dev/null)
|
||||
'import sys,json,re,datetime
|
||||
d=json.load(sys.stdin) or []
|
||||
t=lambda x: datetime.datetime.fromisoformat(re.sub(r"Z$","+00:00",re.sub(r"(\.\d{6})\d+",r"\1",x["time"]))).timestamp()
|
||||
print(max(d,key=t)["time"] if d else "")' 2>/dev/null)
|
||||
if [[ -z "${horodatage}" ]]; then
|
||||
# ON NE DISTINGUE PAS ICI « depot vide » de « depot injoignable », et c'est voulu :
|
||||
# dans les deux cas il n'y a PAS de sauvegarde exploitable, et c'est ce qui compte.
|
||||
|
|
|
|||
|
|
@ -62,9 +62,14 @@ for noeud in {{ serveur_backup_noeuds_attendus | sort | join(' ') }}; do
|
|||
continue
|
||||
fi
|
||||
|
||||
# Le plus recent de tous les groupes (machine ET liste de chemins), pas le premier : voir
|
||||
# `client_backup/templates/verifier-mon-depot.sh.j2` (2026-10-08).
|
||||
json=$(restic -r "${depot}" snapshots --latest 1 --json 2>/dev/null)
|
||||
horodatage=$(printf '%s' "${json}" | python3 -c \
|
||||
'import sys,json;d=json.load(sys.stdin);print(d[0]["time"] if d else "")' 2>/dev/null)
|
||||
'import sys,json,re,datetime
|
||||
d=json.load(sys.stdin) or []
|
||||
t=lambda x: datetime.datetime.fromisoformat(re.sub(r"Z$","+00:00",re.sub(r"(\.\d{6})\d+",r"\1",x["time"]))).timestamp()
|
||||
print(max(d,key=t)["time"] if d else "")' 2>/dev/null)
|
||||
if [[ -z "${horodatage}" ]]; then
|
||||
rapporter "${noeud}" 2 "AUCUN INSTANTANE dans ${depot} — depot present mais vide."
|
||||
continue
|
||||
|
|
|
|||
|
|
@ -327,8 +327,11 @@ def prometheus_loki() -> None:
|
|||
ecrire(vivant / "chunks" / "fake" / "c", "3")
|
||||
argv = ["--avant", str(d / "avant"), "--instantane", "x", "--jeu", f"loki={staging}",
|
||||
"--vivant", f"loki={vivant}"]
|
||||
# L'index que le compacteur fusionne n'est pas un morceau : sa disparition ne compte pas.
|
||||
ecrire(avant / "chunks" / "index" / "index_1" / "1791478851-obs-01.tsdb.gz", "index")
|
||||
rc, out = lancer(argv)
|
||||
verifier(rc == 0 and "1 morceau(x) nouveau(x)" in out, "Loki restaure, des journaux arrives depuis : sans ecart")
|
||||
verifier(rc == 0 and "1 morceau(x) nouveau(x)" in out and "retire" not in out,
|
||||
f"Loki restaure, des journaux arrives depuis, un index compacte : sans ecart ni retrait ({out.strip()[-100:]})")
|
||||
(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")
|
||||
|
|
|
|||
91
scripts/tests/test_verifier_depot.py
Normal file
91
scripts/tests/test_verifier_depot.py
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
#!/usr/bin/env python3
|
||||
"""La sonde du depot juge le PLUS RECENT instantane, pas le premier groupe que restic rend.
|
||||
|
||||
POURQUOI (2026-10-08). `restic snapshots --latest 1` rend le dernier instantane de chaque
|
||||
groupe — machine ET liste de chemins. Ajouter un jeu a une machine change sa liste : deux
|
||||
groupes. La sonde gardait `d[0]` : sur site-mon-01, au lendemain de l'arrivee de prometheus,
|
||||
loki et icinga, elle annoncait « EN RETARD : 26 h » sur une sauvegarde de 2 h 30, et aurait
|
||||
annonce « PERIME » a 50 h. La sonde rendue depuis son vrai gabarit, `restic` et `curl` doubles.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
import stat
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import jinja2
|
||||
|
||||
RACINE = Path(__file__).resolve().parents[2]
|
||||
GABARIT = RACINE / "roles/client_backup/templates/verifier-mon-depot.sh.j2"
|
||||
ECHECS: list[str] = []
|
||||
|
||||
|
||||
def verifier(cond: bool, msg: str) -> None:
|
||||
print(("OK " if cond else "ECHEC ") + msg)
|
||||
if not cond:
|
||||
ECHECS.append(msg)
|
||||
|
||||
|
||||
def executable(p: Path, texte: str) -> None:
|
||||
p.write_text(texte)
|
||||
p.chmod(p.stat().st_mode | stat.S_IXUSR)
|
||||
|
||||
|
||||
def lancer(d: Path, groupes: list[dict]) -> dict:
|
||||
bin_ = d / "bin"
|
||||
bin_.mkdir(exist_ok=True)
|
||||
(d / "groupes.json").write_text(json.dumps(groupes))
|
||||
executable(bin_ / "restic", f"""#!/bin/bash
|
||||
[[ "$1" == --retry-lock ]] && shift 2
|
||||
case "$1" in
|
||||
snapshots) cat "{d}/groupes.json" ;;
|
||||
stats) echo '{{"total_size": 1234}}' ;;
|
||||
ls) echo '{{"struct_type":"node","type":"file"}}' ;;
|
||||
esac
|
||||
""")
|
||||
executable(bin_ / "curl", f"""#!/bin/bash
|
||||
while (( $# )); do [[ "$1" == -d ]] && echo "$2" > "{d}/verdict.json"; shift; done
|
||||
echo '{{"results":[{{"code": 200}}]}}'
|
||||
""")
|
||||
(d / "mdp").write_text("x")
|
||||
rendu = jinja2.Environment(undefined=jinja2.StrictUndefined).from_string(GABARIT.read_text()).render(
|
||||
client_backup_attente_verrou="30m", client_backup_repo="sftp:r@d:h", client_backup_icinga_hote="mon-01",
|
||||
domaine_interne="exemple.internal", client_backup_ttl_icinga=21600, client_backup_age_warn_h=26,
|
||||
client_backup_age_crit_h=50, inventory_hostname="site-mon-01", client_backup_jobs=[{"chemins": [str(d / "x")]}],
|
||||
client_backup_ca_verification="/dev/null", client_backup_icinga_utilisateur="u",
|
||||
).replace("/etc/setops/icinga-api.pass", str(d / "mdp")).replace("/etc/setops/restic.pass", str(d / "mdp"))
|
||||
executable(d / "sonde.sh", rendu)
|
||||
subprocess.run(["bash", str(d / "sonde.sh")], env={**os.environ, "PATH": f"{bin_}:{os.environ['PATH']}"},
|
||||
capture_output=True, text=True)
|
||||
return json.loads((d / "verdict.json").read_text()) if (d / "verdict.json").exists() else {}
|
||||
|
||||
|
||||
def heure(il_y_a_h: float) -> str:
|
||||
t = datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(hours=il_y_a_h)
|
||||
return t.astimezone().isoformat(timespec="microseconds")[:-6] + "123" + t.astimezone().isoformat()[-6:]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
v = lancer(Path(d), [{"time": heure(30), "paths": ["/var/backups/setops/postgresql"]},
|
||||
{"time": heure(1), "paths": ["/var/backups/setops/postgresql", "/var/backups/setops/loki"]}])
|
||||
verifier(v.get("exit_status") == 0 and "OK" in v.get("plugin_output", ""),
|
||||
f"deux groupes, l'ancien en premier : la sonde juge le plus recent ({v.get('plugin_output')})")
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
v = lancer(Path(d), [{"time": heure(30), "paths": ["/a"]}])
|
||||
verifier(v.get("exit_status") == 1 and "EN RETARD" in v.get("plugin_output", ""),
|
||||
f"un seul groupe, vieux de 30 h : toujours EN RETARD ({v.get('plugin_output')})")
|
||||
if ECHECS:
|
||||
print(f"\n{len(ECHECS)} echec(s).")
|
||||
return 1
|
||||
print("\nLa sonde juge le plus recent instantane, quel que soit l'ordre des groupes.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Loading…
Reference in a new issue