import os import re import subprocess def generate_key(dest_name, data_dir): """Génère une paire de clés ed25519 pour cette destination si elle n'existe pas encore.""" key_name = f"dest_{_slugify(dest_name)}_ed25519" os.makedirs(os.path.join(data_dir, "keys"), exist_ok=True) key_path = os.path.join(data_dir, "keys", key_name) if not os.path.exists(key_path): subprocess.run( [ "ssh-keygen", "-t", "ed25519", "-f", key_path, "-N", "", "-C", f"backupmanager@{dest_name}", ], check=True, capture_output=True, ) os.chmod(key_path, 0o600) return key_name def get_public_key(key_name, data_dir): """Retourne la clé publique à copier dans authorized_keys sur le serveur distant.""" pub_path = os.path.join(data_dir, "keys", key_name + ".pub") try: with open(pub_path) as f: return f.read().strip() except FileNotFoundError: return None def transfer_archive(archive_name, destination, backup_dir, data_dir): """Transfère .tar + .info.json vers la destination via rsync SSH.""" key_path = os.path.join(data_dir, "keys", destination.key_name) if not os.path.exists(key_path): raise FileNotFoundError( f"Clé SSH introuvable : {key_path}\n" "Accédez à la page de la destination pour afficher la clé publique." ) from jobs.utils import sudo_exists files = [] for ext in (".tar", ".info.json"): path = os.path.join(backup_dir, archive_name + ext) if sudo_exists(path): files.append(path) if not files: raise FileNotFoundError(f"Aucun fichier à transférer pour {archive_name}.") remote = f"{destination.user}@{destination.host}:{destination.remote_path}/" ssh_opts = ( f"ssh -i {key_path} -p {destination.port}" " -o StrictHostKeyChecking=accept-new" " -o BatchMode=yes" " -o ConnectTimeout=30" ) # sudo car les archives YNH (ynh_app/ynh_system) sont owned root cmd = ["sudo", "rsync", "-az", "--stats", "-e", ssh_opts] + files + [remote] result = subprocess.run(cmd, capture_output=True, text=True, timeout=7200) log = (result.stdout + result.stderr).strip() if result.returncode != 0: raise RuntimeError( f"rsync vers {destination.host} échoué (code {result.returncode}) :\n{log}" ) return log def test_connection(destination, data_dir): """Teste la connexion SSH vers la destination. Retourne (True, msg) ou (False, msg).""" key_path = os.path.join(data_dir, "keys", destination.key_name) if not os.path.exists(key_path): return False, "Clé SSH non générée. Affichez la destination pour la créer." result = subprocess.run( [ "ssh", "-i", key_path, "-p", str(destination.port), "-o", "StrictHostKeyChecking=accept-new", "-o", "BatchMode=yes", "-o", "ConnectTimeout=10", f"{destination.user}@{destination.host}", "echo ok", ], capture_output=True, text=True, timeout=20, ) if result.returncode == 0: return True, f"Connexion réussie vers {destination.host}:{destination.port}." return False, result.stderr.strip() or f"Connexion échouée (code {result.returncode})." def push_archive_to_instance(archive_name, instance, backup_dir, job=None): """Pousse .tar + .info.json vers une instance fédérée via HTTP chunked. Retourne un log texte.""" import hashlib from federation.client import FederationClient from jobs.utils import sudo_exists archive_path = os.path.join(backup_dir, archive_name + ".tar") tmp_tar = f"/tmp/backupmanager_push_{archive_name}.tar" tmp_info = f"/tmp/backupmanager_push_{archive_name}.info.json" try: result = subprocess.run(["sudo", "rsync", archive_path, tmp_tar], capture_output=True, text=True) if result.returncode != 0: raise RuntimeError(f"Copie locale échouée : {result.stderr.strip()}") total_size = os.path.getsize(tmp_tar) sha256 = hashlib.sha256() chunk_size = 50 * 1024 * 1024 with open(tmp_tar, "rb") as f: while True: data = f.read(65536) if not data: break sha256.update(data) checksum = sha256.hexdigest() client = FederationClient(instance) upload_info = client.upload_start(archive_name + ".tar", total_size, checksum, chunk_size) upload_id = upload_info["upload_id"] with open(tmp_tar, "rb") as f: n = 0 while True: data = f.read(chunk_size) if not data: break client.upload_chunk(upload_id, n, data) n += 1 info_json_content = None info_path = os.path.join(backup_dir, archive_name + ".info.json") if sudo_exists(info_path): r = subprocess.run(["sudo", "rsync", info_path, tmp_info], capture_output=True) if r.returncode == 0: try: with open(tmp_info, "r", encoding="utf-8", errors="replace") as fh: info_json_content = fh.read() finally: subprocess.run(["sudo", "rm", "-rf", tmp_info], capture_output=True) client.upload_finish_with_info(upload_id, info_json_content) log = f"Transfert HTTP chunked vers {instance.name} ({instance.url}) — {total_size // (1024*1024)} Mo en {n} chunks." if job is not None: from retention import apply_remote_retention try: deleted = apply_remote_retention(job, client) if deleted: log += f"\nRétention distante : {len(deleted)} archive(s) supprimée(s) : {', '.join(deleted)}" except Exception as exc: log += f"\n⚠ Rétention distante échouée : {exc}" return log finally: if os.path.exists(tmp_tar): os.unlink(tmp_tar) def _slugify(s): return re.sub(r'[^a-z0-9]+', '-', s.lower().strip()).strip('-')