fix(ssh): support .ppk via puttygen auto-conversion + messagebox d'erreur si cle non lisible (fix connexion silencieuse vtdsi*)

This commit is contained in:
Pierre & Lumière 2026-04-29 14:30:26 +02:00
parent fb29b59625
commit e5ce3b2ec9

View File

@ -670,15 +670,72 @@ def build_fqdn(server, env):
return [server, f"{server}.sanef.groupe"] return [server, f"{server}.sanef.groupe"]
def load_key(keyfile): 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 return None
keyfile = keyfile.strip('"').strip("'") 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]: for cls in [paramiko.RSAKey, paramiko.Ed25519Key, paramiko.ECDSAKey]:
try: try:
return cls.from_private_key_file(keyfile) return cls.from_private_key_file(keyfile)
except Exception: except Exception as ex:
last_ex = ex
continue 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 return None
load_key.last_error = None
def ssh_connect(server, env, settings, pkey, pkey2, cyb_password=None): def ssh_connect(server, env, settings, pkey, pkey2, cyb_password=None):
"""Connexion SSH avec fallback FQDN et clé. Retourne (client, hostname_effectif)""" """Connexion SSH avec fallback FQDN et clé. Retourne (client, hostname_effectif)"""
@ -2294,8 +2351,28 @@ class PatchManagerV2:
self._reload_keys() self._reload_keys()
def _reload_keys(self): def _reload_keys(self):
self.pkey = load_key(self.settings.get("keyfile","")) errors = []
self.pkey2 = load_key(self.settings.get("keyfile2","")) 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 # ONGLET 1 — LOGIQUE