#!/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())