diff --git a/app/scripts/backup_prod.py b/app/scripts/backup_prod.py new file mode 100644 index 0000000..65c45ee --- /dev/null +++ b/app/scripts/backup_prod.py @@ -0,0 +1,127 @@ +""" +Sauvegarde quotidienne de la base de production — export logique daté. + +Le plan Render pro fournit déjà des sauvegardes physiques automatiques (reprise +rapide, restauration point-dans-le-temps). Ce script les COMPLÈTE par un export +logique téléchargeable : une copie portable, hors Render, qu'un dossier BCRG +peut archiver et vérifier. Une sauvegarde qu'on ne peut pas produire à la +demande et constater n'en est pas une. + +Déclenché par un cron Render (voir render.yaml). Envoie une trace Brevo à la +Conformité avec l'horodatage et l'URL de téléchargement de l'export. + +Variables d'environnement : + RENDER_API_KEY clé API Render (compte). SANS elle, le script s'abstient + proprement (comme Brevo) plutôt que d'échouer. + RENDER_PROD_DB_ID identifiant de la base de production (dpg-…). + BREVO_* configuration SMTP pour la trace (partagée avec les listes). + +⚠ La clé API Render est à portée COMPTE : la stocker ici donne au cron un accès + complet. À n'activer qu'en connaissance de cause ; à faire tourner/retirer si + Render introduit un jour des clés à portée réduite. +""" +from __future__ import annotations + +import logging +import os +import time +from datetime import datetime, timezone + +import httpx + +from app.services import list_notifier + +logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") +logger = logging.getLogger("backup_prod") + +RENDER_API = "https://api.render.com/v1" +API_KEY = os.getenv("RENDER_API_KEY") +DB_ID = os.getenv("RENDER_PROD_DB_ID") + +# Fenêtre d'attente : un export logique met quelques minutes à apparaître. +POLL_TIMEOUT_S = 600 +POLL_INTERVAL_S = 20 + + +def _headers() -> dict: + return {"Authorization": f"Bearer {API_KEY}", "Accept": "application/json"} + + +def _list_exports() -> list[dict]: + r = httpx.get(f"{RENDER_API}/postgres/{DB_ID}/export", headers=_headers(), timeout=30) + r.raise_for_status() + return r.json() or [] + + +def _trigger_export() -> None: + r = httpx.post(f"{RENDER_API}/postgres/{DB_ID}/export", headers=_headers(), timeout=30) + # 201/202 selon l'état ; on tolère, l'apparition dans la liste fait foi. + if r.status_code >= 400: + raise RuntimeError(f"déclenchement refusé : HTTP {r.status_code} {r.text[:200]}") + + +def _notify(ok: bool, detail: str, url: str | None = None) -> None: + ts = datetime.now(timezone.utc).strftime("%d/%m/%Y %H:%M UTC") + etat, couleur = ("SUCCÈS", "#15803d") if ok else ("ÉCHEC", "#b91c1c") + lien = (f"
Télécharger l'export " + f"(lien signé, à durée limitée)
") if url else "" + html = f""" +{ts} · + {etat}
+{detail}
+ {lien} ++ Export logique complémentaire des sauvegardes physiques Render. + Copie portable destinée à l'archivage de conformité. +
+