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>
91 lines
3.9 KiB
Python
91 lines
3.9 KiB
Python
#!/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())
|