@@ -1452,6 +1452,22 @@ def _safe_component(value: str) -> str:
14521452 return cleaned or "vm"
14531453
14541454
1455+ _HOST_KEY_CHANGED_MARKERS = (
1456+ b"Host key verification failed" ,
1457+ b"REMOTE HOST IDENTIFICATION HAS CHANGED" ,
1458+ )
1459+
1460+
1461+ def _is_host_key_changed (stderr_bytes : Optional [bytes ]) -> bool :
1462+ """True si un stderr SSH signale une clé hote qui a CHANGE (pas juste
1463+ une machine jamais vue : StrictHostKeyChecking=accept-new, deja pose
1464+ dans _connection_options, accepte deja celle-la sans broncher). Le cas
1465+ courant avec une VM ephemere (Hetzner...) reconstruite/reinstallee
1466+ depuis la derniere connexion, pas une anomalie."""
1467+ data = stderr_bytes or b""
1468+ return any (marker in data for marker in _HOST_KEY_CHANGED_MARKERS )
1469+
1470+
14551471class VmController :
14561472 def __init__ (self , options : Options , deps : Optional [RuntimeDeps ] = None ):
14571473 self .options = options
@@ -1528,6 +1544,16 @@ def reset_host_key(self) -> None:
15281544 if completed .returncode not in (0 , 1 ):
15291545 raise RunOnVmError ("impossible de supprimer l'ancienne clé SSH" )
15301546
1547+ def _auto_reset_host_key (self ) -> None :
1548+ """Appelé UNIQUEMENT après un échec SSH confirmé (_is_host_key_changed)
1549+ avant de retenter une fois : annonce toujours l'action (rien de
1550+ silencieux), même si elle est automatique."""
1551+ host = self .options .vm .rsplit ("@" , 1 )[- 1 ]
1552+ print (f" SSH: host key for { host } has changed (VM rebuilt/reinstalled?) "
1553+ f"- clearing the stale known_hosts entry and retrying once..." ,
1554+ flush = True )
1555+ self .reset_host_key ()
1556+
15311557 def query_state (self ) -> RemoteState :
15321558 completed = subprocess .run (
15331559 self ._ssh_command ([self .options .session ]),
@@ -1536,6 +1562,15 @@ def query_state(self) -> RemoteState:
15361562 stderr = subprocess .PIPE ,
15371563 check = False ,
15381564 )
1565+ if completed .returncode != 0 and _is_host_key_changed (completed .stderr ):
1566+ self ._auto_reset_host_key ()
1567+ completed = subprocess .run (
1568+ self ._ssh_command ([self .options .session ]),
1569+ input = REMOTE_QUERY_SCRIPT .encode ("utf-8" ),
1570+ stdout = subprocess .PIPE ,
1571+ stderr = subprocess .PIPE ,
1572+ check = False ,
1573+ )
15391574 if completed .returncode != 0 :
15401575 stderr = completed .stderr .decode ("utf-8" , errors = "replace" ).strip ()
15411576 raise SshError (
@@ -1566,9 +1601,19 @@ def launch(self) -> None:
15661601 completed = subprocess .run (
15671602 self ._ssh_command (remote_args ),
15681603 input = REMOTE_LAUNCH_SCRIPT .encode ("utf-8" ),
1604+ stderr = subprocess .PIPE ,
15691605 check = False ,
15701606 )
1607+ if completed .returncode != 0 and _is_host_key_changed (completed .stderr ):
1608+ self ._auto_reset_host_key ()
1609+ completed = subprocess .run (
1610+ self ._ssh_command (remote_args ),
1611+ input = REMOTE_LAUNCH_SCRIPT .encode ("utf-8" ),
1612+ stderr = subprocess .PIPE ,
1613+ check = False ,
1614+ )
15711615 if completed .returncode != 0 :
1616+ sys .stderr .buffer .write (completed .stderr or b"" )
15721617 raise RunOnVmError (
15731618 "le lancement distant a échoué (code {})" .format (
15741619 completed .returncode
0 commit comments