Bladeren bron

feat: rétention distante SSH après transfert

Applique la même politique count/daily/GFS sur le serveur SSH distant
après chaque transfert réussi. Les échecs sont loggés sans bloquer le job.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Cedric Hansen 2 weken geleden
bovenliggende
commit
60a005f7f0
2 gewijzigde bestanden met toevoegingen van 65 en 0 verwijderingen
  1. 7 0
      sources/jobs/ynh_backup.py
  2. 58 0
      sources/retention.py

+ 7 - 0
sources/jobs/ynh_backup.py

@@ -72,6 +72,13 @@ def execute_job(job_id):
                     from jobs.transfer import transfer_archive
                     transfer_log = transfer_archive(archive_name, obj, backup_dir, data_dir)
                     run.log_text += f"\n{transfer_log}"
+                    try:
+                        from retention import apply_ssh_retention
+                        ssh_deleted = apply_ssh_retention(job, obj, data_dir)
+                        if ssh_deleted:
+                            run.log_text += f"\nRétention distante SSH : {len(ssh_deleted)} archive(s) supprimée(s) : {', '.join(ssh_deleted)}"
+                    except Exception as ret_exc:
+                        run.log_text += f"\n⚠ Rétention distante SSH échouée : {ret_exc}"
                 except Exception as transfer_exc:
                     run.log_text += f"\n⚠ Transfert échoué : {transfer_exc}"
                     transfer_errors += 1

+ 58 - 0
sources/retention.py

@@ -1,5 +1,7 @@
 import os
 import re
+import shlex
+import subprocess
 from datetime import datetime, timedelta
 
 
@@ -65,6 +67,62 @@ def _list_archives_for_job(job, backup_dir):
     return archives
 
 
+def apply_ssh_retention(job, destination, data_dir):
+    """Applique la rétention sur une destination SSH après un transfert réussi."""
+    import json as _json
+    from flask import current_app
+
+    instance = current_app.config["INSTANCE_NAME"]
+    prefix = _job_archive_prefix(job, instance)
+    key_path = os.path.join(data_dir, "keys", destination.key_name)
+
+    ssh_base = [
+        "ssh",
+        "-i", key_path,
+        "-p", str(destination.port),
+        "-o", "StrictHostKeyChecking=accept-new",
+        "-o", "BatchMode=yes",
+        "-o", "ConnectTimeout=30",
+        f"{destination.user}@{destination.host}",
+    ]
+
+    remote_dir = shlex.quote(destination.remote_path)
+    result = subprocess.run(
+        ssh_base + [f"find {remote_dir} -maxdepth 1 -name '*.tar' -printf '%f\\n' 2>/dev/null || true"],
+        capture_output=True, text=True, timeout=30,
+    )
+    if result.returncode != 0:
+        raise RuntimeError(f"Listage distant échoué : {result.stderr.strip()}")
+
+    remote_archives = sorted(
+        [f for f in result.stdout.splitlines() if f.startswith(prefix) and f.endswith(".tar")],
+        key=_extract_date,
+    )
+
+    if job.retention_mode == "count":
+        to_delete = _retention_count(remote_archives, job.retention_value)
+    elif job.retention_mode == "daily":
+        to_delete = _retention_daily(remote_archives, job.retention_value)
+    elif job.retention_mode == "gfs":
+        cfg = _json.loads(job.retention_gfs_config or "{}") if job.retention_gfs_config else {}
+        to_delete = _retention_gfs(remote_archives, cfg)
+    else:
+        return []
+
+    deleted = []
+    for archive_filename in to_delete:
+        base = os.path.splitext(archive_filename)[0]
+        tar_q = shlex.quote(f"{destination.remote_path}/{base}.tar")
+        info_q = shlex.quote(f"{destination.remote_path}/{base}.info.json")
+        r = subprocess.run(
+            ssh_base + [f"rm -f {tar_q} {info_q}"],
+            capture_output=True, timeout=30,
+        )
+        if r.returncode == 0:
+            deleted.append(archive_filename)
+    return deleted
+
+
 def apply_remote_retention(job, client):
     """Applique la rétention sur l'instance distante après un push.