diff --git a/patch_manager_v2.py b/patch_manager_v2.py index 46a9438..13e7b5b 100644 --- a/patch_manager_v2.py +++ b/patch_manager_v2.py @@ -670,15 +670,72 @@ def build_fqdn(server, env): return [server, f"{server}.sanef.groupe"] def load_key(keyfile): - if not keyfile or not os.path.exists(keyfile.strip('"').strip("'")): + """Charge une clé SSH privée. Retourne l'objet PKey ou None. + Si format PuTTY (.ppk), tente conversion auto via puttygen vers OpenSSH PEM. + Stocke le détail de l'échec dans load_key.last_error pour affichage UI.""" + load_key.last_error = None + if not keyfile: return None keyfile = keyfile.strip('"').strip("'") + if not os.path.exists(keyfile): + load_key.last_error = f"Fichier clé introuvable : {keyfile}" + return None + + # Détection format PuTTY (.ppk) + try: + with open(keyfile, "r", encoding="utf-8", errors="ignore") as f: + first_line = f.readline().strip() + except Exception as ex: + load_key.last_error = f"Lecture impossible : {ex}" + return None + + is_ppk = first_line.startswith("PuTTY-User-Key-File-") + if is_ppk: + # Paramiko ne lit pas .ppk nativement → tenter conversion auto via puttygen + converted = keyfile + ".openssh" + puttygen_paths = [ + r"C:\Program Files\PuTTY\puttygen.exe", + r"C:\Program Files (x86)\PuTTY\puttygen.exe", + "puttygen", # si dans PATH + "puttygen.exe", + ] + import subprocess + converted_ok = False + for pg in puttygen_paths: + try: + subprocess.run( + [pg, keyfile, "-O", "private-openssh", "-o", converted], + check=True, capture_output=True, timeout=10, + ) + converted_ok = True + break + except (FileNotFoundError, subprocess.SubprocessError, OSError): + continue + if not converted_ok: + load_key.last_error = ( + f"Format PuTTY (.ppk) détecté pour {os.path.basename(keyfile)}. " + f"Paramiko ne lit pas ce format nativement et puttygen.exe est introuvable. " + f"Convertir manuellement : " + f'puttygen "{keyfile}" -O private-openssh -o "{keyfile}.openssh" ' + f"puis pointer la conf vers le fichier converti." + ) + return None + keyfile = converted + + # Tenter chargement PEM/OpenSSH (multi-format) + last_ex = None for cls in [paramiko.RSAKey, paramiko.Ed25519Key, paramiko.ECDSAKey]: try: return cls.from_private_key_file(keyfile) - except Exception: + except Exception as ex: + last_ex = ex continue + load_key.last_error = ( + f"Aucun loader paramiko n'a pu lire {os.path.basename(keyfile)} " + f"(RSA / Ed25519 / ECDSA tentés). Dernière erreur : {last_ex}" + ) return None +load_key.last_error = None def ssh_connect(server, env, settings, pkey, pkey2, cyb_password=None): """Connexion SSH avec fallback FQDN et clé. Retourne (client, hostname_effectif)""" @@ -2294,8 +2351,28 @@ class PatchManagerV2: self._reload_keys() def _reload_keys(self): - self.pkey = load_key(self.settings.get("keyfile","")) - self.pkey2 = load_key(self.settings.get("keyfile2","")) + errors = [] + kf1 = self.settings.get("keyfile", "") + if kf1: + self.pkey = load_key(kf1) + if self.pkey is None and load_key.last_error: + errors.append(f"Clé 1 ({kf1}) :\n{load_key.last_error}") + else: + self.pkey = None + kf2 = self.settings.get("keyfile2", "") + if kf2: + self.pkey2 = load_key(kf2) + if self.pkey2 is None and load_key.last_error: + errors.append(f"Clé 2 ({kf2}) :\n{load_key.last_error}") + else: + self.pkey2 = None + if errors: + try: + messagebox.showwarning("Clés SSH non chargées", "\n\n".join(errors)) + except Exception: + # Pas de UI dispo (lancement CLI ?), juste print + for e in errors: + print(f"[load_key] {e}") # ========================================================================== # ONGLET 1 — LOGIQUE