#!/usr/bin/env python3 """Erstelle ein Binary-Backup + einen Text-Export von ap1 vor dem TP15-Cutover. Creds aus ~/.claude.json (mikromcp-env). Backup-Dateien landen unter routeros/. 🔴 PASSWORT NIE UEBER argv. `sshpass -p ` schreibt es in die Kommandozeile, wo es in `ps` und jedem argv-Trace steht -- genau so ist am 2026-08-22 das Core-Passwort in ein Sitzungstranskript geraten und musste rotiert werden. `sshpass -e` liest es aus der Umgebung des Kindprozesses. Umgestellt 2026-08-31. 🔴 AP1_IP 10.40.10.1 ist NICHT ap1 -- das ist die VLAN10-SVI des CORE (live geprueft 2026-08-31: tcp/22 offen). ap1 ist seit dem Core-Cutover 10.40.99.11. Dieses Skript wuerde also den CORE sichern und die Datei "ap1-pre-core-cutover-*.backup" nennen -- es scheitert nicht, es benennt falsch. VOR DEM NAECHSTEN LAUF die IP korrigieren. Bewusst nicht stillschweigend geaendert, s. Handoff 2026-08-31. """ import json import os import pathlib import subprocess import sys from datetime import datetime, timezone AP1_IP = "10.40.10.1" CLAUDE_JSON = pathlib.Path.home() / ".claude.json" OUT_DIR = pathlib.Path(__file__).resolve().parent def load_ap1_creds(): cfg = json.loads(CLAUDE_JSON.read_text()) env = cfg.get("mcpServers", {}).get("mikromcp", {}).get("env", {}) user = env.get("ROUTER_AP1_USER") or env.get("ROUTER_USER") pw = env.get("ROUTER_AP1_PASS") or env.get("ROUTER_PASS") if not user or not pw: raise SystemExit("ROUTER_AP1_USER/PASS nicht in ~/.claude.json mikromcp.env gefunden") return user, pw # Das Passwort geht ausschliesslich ueber die Umgebung an sshpass -- nie als Argument. def ssh_env(pw): return dict(os.environ, SSHPASS=pw) def ssh_cmd(user, remote_command): return [ "sshpass", "-e", "ssh", "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null", "-o", "PubkeyAuthentication=no", f"{user}@{AP1_IP}", remote_command, ] def scp_cmd(user, remote_file, local_file): return [ "sshpass", "-e", "scp", "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null", "-o", "PubkeyAuthentication=no", f"{user}@{AP1_IP}:{remote_file}", str(local_file), ] def main(): user, pw = load_ap1_creds() ts = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S") backup_name = f"ap1-pre-core-cutover-{ts}" print(f"ap1 Backup via SSH {user}@{AP1_IP}") # 1) Binary backup + Text-Export auf ap1 erzeugen create_cmd = ssh_cmd( user, f'/system backup save name="{backup_name}" dont-encrypt=yes; ' f'/export file="{backup_name}"' ) print(f" Erzeuge Backup/Export auf ap1 ...") subprocess.run(create_cmd, check=True, env=ssh_env(pw)) # 2) Herunterladen local_backup = OUT_DIR / f"{backup_name}.backup" local_export = OUT_DIR / f"{backup_name}.rsc" print(f" Lade {backup_name}.backup herunter ...") subprocess.run(scp_cmd(user, f"{backup_name}.backup", local_backup), check=True, env=ssh_env(pw)) print(f" Lade {backup_name}.rsc herunter ...") subprocess.run(scp_cmd(user, f"{backup_name}.rsc", local_export), check=True, env=ssh_env(pw)) # 3) Verifizieren backup_size = local_backup.stat().st_size export_size = local_export.stat().st_size print(f" OK: {local_backup.name} ({backup_size} Bytes)") print(f" OK: {local_export.name} ({export_size} Bytes)") if backup_size < 1000: print(" WARN: Backup-Datei auffällig klein!") sys.exit(1) print("\nNächster Schritt: Cutover-Config auf crs326-core laden.") if __name__ == "__main__": main()