diff --git a/f/Backup/backup_restore_orchestrator__flow/ersten_restore_pro_server_starten.py b/f/Backup/backup_restore_orchestrator__flow/ersten_restore_pro_server_starten.py index 988c6de..6ad32de 100644 --- a/f/Backup/backup_restore_orchestrator__flow/ersten_restore_pro_server_starten.py +++ b/f/Backup/backup_restore_orchestrator__flow/ersten_restore_pro_server_starten.py @@ -1,14 +1,32 @@ -import wmill, json, paramiko, mysql.connector +import wmill, json, paramiko, io, mysql.connector + + +def _load_pkey(key_str: str): + for cls in [paramiko.RSAKey, paramiko.Ed25519Key, paramiko.ECDSAKey, paramiko.DSSKey]: + try: + return cls.from_private_key(io.StringIO(key_str)) + except Exception: + continue + return None + +def _ssh_connect(ip: str, user: str, password: str = "", private_key: str = "") -> paramiko.SSHClient: + ssh = paramiko.SSHClient() + ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + if private_key: + ssh.connect(ip, username=user, pkey=_load_pkey(private_key)) + else: + ssh.connect(ip, username=user, password=password) + return ssh def start_restore(server, backup, job_uuid, webhook_url, webhook_tok): """Startet restore.sh auf einem Server non-blocking.""" - ssh = paramiko.SSHClient() - ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - ssh.connect( + creds = server["ssh_creds"] + ssh = _ssh_connect( server["ip"], - username=server["ssh_creds"]["user"], - password=server["ssh_creds"]["password"] + creds["user"], + creds.get("password", ""), + creds.get("private_key", ""), ) safe_client = backup["client_name"].replace("/", "_").replace(":", "_") srv_hostname = server["hostname"] diff --git a/f/Backup/backup_restore_orchestrator__flow/flow.yaml b/f/Backup/backup_restore_orchestrator__flow/flow.yaml index 8d5cbc5..e644581 100644 --- a/f/Backup/backup_restore_orchestrator__flow/flow.yaml +++ b/f/Backup/backup_restore_orchestrator__flow/flow.yaml @@ -45,8 +45,19 @@ value: expr: results.a lock: '!inline alle_freien_restore-server_holen.lock' language: python3 + - id: i + summary: SSH-Key aus DB testen + value: + type: rawscript + content: '!inline ssh_key_versuch.py' + input_transforms: + prev: + type: javascript + expr: results.b + lock: '!inline ssh_key_versuch.lock' + language: python3 - id: g - summary: SSH-Credentials fuer alle Restore-Server aus Bitwarden + summary: SSH-Credentials fuer alle Restore-Server aus Bitwarden (Fallback) value: type: rawscript content: '!inline ssh-credentials_fuer_alle_restore-server_aus_bitwarden.py' @@ -56,7 +67,7 @@ value: value: https://bitwarden.stines.de prev: type: javascript - expr: results.b + expr: results.i lock: '!inline ssh-credentials_fuer_alle_restore-server_aus_bitwarden.lock' language: python3 - id: c diff --git a/f/Backup/backup_restore_orchestrator__flow/script_deployen_&_pbs-datastores_auf_allen_servern_registrieren.py b/f/Backup/backup_restore_orchestrator__flow/script_deployen_&_pbs-datastores_auf_allen_servern_registrieren.py index a5b88da..5a3442b 100644 --- a/f/Backup/backup_restore_orchestrator__flow/script_deployen_&_pbs-datastores_auf_allen_servern_registrieren.py +++ b/f/Backup/backup_restore_orchestrator__flow/script_deployen_&_pbs-datastores_auf_allen_servern_registrieren.py @@ -1,5 +1,23 @@ import wmill, json, paramiko, io, mysql.connector, re +def _load_pkey(key_str: str): + for cls in [paramiko.RSAKey, paramiko.Ed25519Key, paramiko.ECDSAKey, paramiko.DSSKey]: + try: + return cls.from_private_key(io.StringIO(key_str)) + except Exception: + continue + return None + +def _ssh_connect(ip: str, user: str, password: str = "", private_key: str = "") -> paramiko.SSHClient: + ssh = paramiko.SSHClient() + ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + if private_key: + pkey = _load_pkey(private_key) + ssh.connect(ip, username=user, pkey=pkey) + else: + ssh.connect(ip, username=user, password=password) + return ssh + GITEA_REPO = "http://172.17.1.251:8080/sebastian.serfling/BackupScript.git" @@ -147,14 +165,16 @@ def deploy_to_server(ssh, server, pbs, pbs_host, pbs_user, pbs_pass, cur.execute(""" INSERT INTO Kunden.`bronze.restore.session` - (job_uuid, hostname, ip, ssh_user, ssh_password) - VALUES (%s, %s, %s, %s, %s) + (job_uuid, hostname, ip, ssh_user, ssh_password, ssh_private_key) + VALUES (%s, %s, %s, %s, %s, %s) ON DUPLICATE KEY UPDATE ip=VALUES(ip), ssh_user=VALUES(ssh_user), - ssh_password=VALUES(ssh_password) + ssh_password=VALUES(ssh_password), + ssh_private_key=VALUES(ssh_private_key) """, ( job_uuid, hostname, ssh_creds["ip"], ssh_creds["user"], ssh_creds["password"], + ssh_creds.get("private_key", "") or "", )) conn.commit(); cur.close(); conn.close() @@ -207,16 +227,18 @@ def main(prev: dict, bw_result: dict = {}, datastores: list = []): print(f"WARNUNG: Keine IP fuer '{hostname}' – uebersprungen.") continue + ssh_user = creds.get("username", "root") + ssh_password = creds.get("password", "") + ssh_private_key = creds.get("private_key", "") + ssh_creds_dict = { - "ip": ip, - "user": creds.get("username", "root"), - "password": creds.get("password", ""), + "ip": ip, + "user": ssh_user, + "password": ssh_password, + "private_key": ssh_private_key, } - ssh = paramiko.SSHClient() - ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - ssh.connect(ip, username=ssh_creds_dict["user"], - password=ssh_creds_dict["password"]) + ssh = _ssh_connect(ip, ssh_user, ssh_password, ssh_private_key) try: pbs_storages = deploy_to_server( diff --git a/f/Backup/backup_restore_orchestrator__flow/ssh-credentials_fuer_alle_restore-server_aus_bitwarden.py b/f/Backup/backup_restore_orchestrator__flow/ssh-credentials_fuer_alle_restore-server_aus_bitwarden.py index a50350c..c864f49 100644 --- a/f/Backup/backup_restore_orchestrator__flow/ssh-credentials_fuer_alle_restore-server_aus_bitwarden.py +++ b/f/Backup/backup_restore_orchestrator__flow/ssh-credentials_fuer_alle_restore-server_aus_bitwarden.py @@ -34,7 +34,13 @@ def main( if prev.get("mode") == "webhook": return prev - servers = prev.get("target_servers", []) + needs_bitwarden = prev.get("needs_bitwarden", []) + if not needs_bitwarden: + print("Alle Server per SSH-Key authentifiziert – Bitwarden wird nicht benötigt.") + return prev + + servers = [s for s in prev.get("target_servers", []) + if s["hostname"] in needs_bitwarden] bw_creds = json.loads(wmill.get_variable("f/Backup/bitwarden_api_login")) env = os.environ.copy() @@ -69,7 +75,7 @@ def main( raise Exception("Vault konnte nicht entsperrt werden") env["BW_SESSION"] = bw_session - server_creds = {} + server_creds = prev.get("server_creds", {}) for server in servers: hostname = server["hostname"] print(f"Hole Creds fuer: {hostname}") diff --git a/f/Backup/backup_restore_orchestrator__flow/ssh_key_versuch.lock b/f/Backup/backup_restore_orchestrator__flow/ssh_key_versuch.lock new file mode 100644 index 0000000..48ff042 --- /dev/null +++ b/f/Backup/backup_restore_orchestrator__flow/ssh_key_versuch.lock @@ -0,0 +1,17 @@ +# py: 3.12 +anyio==4.12.1 +bcrypt==5.0.0 +certifi==2026.2.25 +cffi==2.0.0 +cryptography==46.0.5 +h11==0.16.0 +httpcore==1.0.9 +httpx==0.28.1 +idna==3.11 +invoke==2.2.1 +mysql-connector-python==9.6.0 +paramiko==4.0.0 +pycparser==3.0 +pynacl==1.6.2 +typing-extensions==4.15.0 +wmill==1.657.2 diff --git a/f/Backup/backup_restore_orchestrator__flow/ssh_key_versuch.py b/f/Backup/backup_restore_orchestrator__flow/ssh_key_versuch.py new file mode 100644 index 0000000..88bfff4 --- /dev/null +++ b/f/Backup/backup_restore_orchestrator__flow/ssh_key_versuch.py @@ -0,0 +1,76 @@ +import wmill, mysql.connector, json, paramiko, io + +def _load_pkey(key_str: str): + for cls in [paramiko.RSAKey, paramiko.Ed25519Key, paramiko.ECDSAKey, paramiko.DSSKey]: + try: + return cls.from_private_key(io.StringIO(key_str)) + except Exception: + continue + return None + +def main(prev: dict): + if prev.get("mode") == "webhook": + return prev + + servers = prev.get("target_servers", []) + db_cfg = json.loads(wmill.get_variable("f/Backup/mysql_config")) + conn = mysql.connector.connect(**db_cfg) + cur = conn.cursor(dictionary=True) + + hostnames = [s["hostname"] for s in servers] + placeholders = ",".join(["%s"] * len(hostnames)) + cur.execute(f""" + SELECT hostname, ip, ssh_private_key, ssh_key_user + FROM Kunden.`bronze.restore.server` + WHERE hostname IN ({placeholders}) + AND ssh_private_key IS NOT NULL + AND ssh_private_key != '' + """, hostnames) + key_rows = {row["hostname"]: row for row in cur.fetchall()} + cur.close(); conn.close() + + server_creds = {} + needs_bitwarden = [] + + for server in servers: + hostname = server["hostname"] + ip = server.get("ip", "") + + if hostname not in key_rows: + print(f"[{hostname}] Kein SSH-Key in DB → Bitwarden") + needs_bitwarden.append(hostname) + continue + + row = key_rows[hostname] + key_str = row["ssh_private_key"] + user = row.get("ssh_key_user") or "root" + actual_ip = row.get("ip") or ip + + pkey = _load_pkey(key_str) + if pkey is None: + print(f"[{hostname}] Key-Format nicht erkannt → Bitwarden") + needs_bitwarden.append(hostname) + continue + + ssh = paramiko.SSHClient() + ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + try: + ssh.connect(actual_ip, username=user, pkey=pkey, + timeout=5, auth_timeout=5) + ssh.close() + print(f"[{hostname}] SSH-Key Auth OK ✓ ({user}@{actual_ip})") + server_creds[hostname] = { + "username": user, + "password": "", + "private_key": key_str, + "auth_method": "key", + "url": f"https://{actual_ip}", + } + except Exception as e: + print(f"[{hostname}] SSH-Key fehlgeschlagen: {e} → Bitwarden") + needs_bitwarden.append(hostname) + + print(f"Key-Auth OK: {list(server_creds.keys())}") + print(f"Braucht Bitwarden: {needs_bitwarden}") + + return {**prev, "server_creds": server_creds, "needs_bitwarden": needs_bitwarden} diff --git a/f/Backup/backup_restore_orchestrator__flow/webhook_verarbeiten_&_naechsten_restore_auf_demselben_server_starten.py b/f/Backup/backup_restore_orchestrator__flow/webhook_verarbeiten_&_naechsten_restore_auf_demselben_server_starten.py index f416e5c..fdafbca 100644 --- a/f/Backup/backup_restore_orchestrator__flow/webhook_verarbeiten_&_naechsten_restore_auf_demselben_server_starten.py +++ b/f/Backup/backup_restore_orchestrator__flow/webhook_verarbeiten_&_naechsten_restore_auf_demselben_server_starten.py @@ -1,4 +1,21 @@ -import wmill, json, mysql.connector, paramiko, re, base64 +import wmill, json, mysql.connector, paramiko, io, re, base64 + +def _load_pkey(key_str: str): + for cls in [paramiko.RSAKey, paramiko.Ed25519Key, paramiko.ECDSAKey, paramiko.DSSKey]: + try: + return cls.from_private_key(io.StringIO(key_str)) + except Exception: + continue + return None + +def _ssh_connect(ip: str, user: str, password: str = "", private_key: str = "") -> paramiko.SSHClient: + ssh = paramiko.SSHClient() + ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + if private_key: + ssh.connect(ip, username=user, pkey=_load_pkey(private_key)) + else: + ssh.connect(ip, username=user, password=password) + return ssh from datetime import datetime @@ -349,7 +366,7 @@ def main(from_init: dict): conn3 = mysql.connector.connect(**db_cfg) cur3 = conn3.cursor(dictionary=True) cur3.execute(""" - SELECT ip, ssh_user, ssh_password + SELECT ip, ssh_user, ssh_password, ssh_private_key FROM Kunden.`bronze.restore.session` WHERE job_uuid = %s AND hostname = %s LIMIT 1 @@ -366,12 +383,11 @@ def main(from_init: dict): webhook_url = wmill.get_variable("f/Backup/windmill_webhook_url") webhook_tok = wmill.get_variable("f/Backup/windmill_webhook_token") - ssh = paramiko.SSHClient() - ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - ssh.connect( + ssh = _ssh_connect( session["ip"], - username=session["ssh_user"], - password=session["ssh_password"] + session["ssh_user"], + session.get("ssh_password", "") or "", + session.get("ssh_private_key", "") or "", ) safe_client = nxt["client_name"].replace("/", "_").replace(":", "_")