55import os
66import platform
77import subprocess
8+ import tempfile
89
910import lib .config as config
1011from lib .netutil import wrap_ip
@@ -24,12 +25,15 @@ def __init__(self, returncode: int, stdout: str, cmd: str | list[str], exception
2425 self .cmd = cmd
2526
2627class SSHCommandFailed (BaseCommandFailed ):
27- def __init__ (self , returncode : int , stdout : str , cmd : str ):
28+ __slots__ = ('ssherr' ,)
29+
30+ def __init__ (self , returncode : int , stdout : str , cmd : str , ssherr : str = '' ):
2831 msg_end = f": { stdout } " if stdout else "."
2932 super (SSHCommandFailed , self ).__init__ (
3033 returncode , stdout , cmd ,
3134 f'SSH command ({ cmd } ) failed with return code { returncode } { msg_end } '
3235 )
36+ self .ssherr = ssherr
3337
3438class LocalCommandFailed (BaseCommandFailed ):
3539 def __init__ (self , returncode : int , stdout : str , cmd : str | list [str ]):
@@ -49,8 +53,11 @@ def __init__(self, returncode: int, stdout: ResultOutputT):
4953 self .stdout : ResultOutputT = stdout
5054
5155class SSHResult (BaseCmdResult [ResultOutputT ]):
52- def __init__ (self , returncode : int , stdout : ResultOutputT ):
56+ __slots__ = ('ssherr' ,)
57+
58+ def __init__ (self , returncode : int , stdout : ResultOutputT , ssherr : str = '' ):
5359 super (SSHResult , self ).__init__ (returncode , stdout )
60+ self .ssherr : str = ssherr
5461
5562class LocalCommandResult (BaseCmdResult [ResultOutputT ]):
5663 def __init__ (self , returncode : int , stdout : ResultOutputT ):
@@ -102,63 +109,80 @@ def _ssh(
102109 else :
103110 opts += ['-o' , 'ControlMaster no' ]
104111
105- ssh_cmd = ['ssh' , f'root@{ hostname_or_ip } ' ] + opts + [cmd ]
106-
107112 # Fetch banner and remove it to avoid stdout/stderr pollution.
108113 banner_res = None
109114 if config .ignore_ssh_banner :
110- banner_res = subprocess .run (
111- ['ssh' , f'root@{ hostname_or_ip } ' ] + opts + ['\n ' ],
112- stdout = subprocess .PIPE ,
113- stderr = subprocess .STDOUT ,
114- check = False
115- )
115+ with tempfile .NamedTemporaryFile (suffix = '.log' , prefix = 'ssh_err_banner_' , mode = 'r' ) as banner_log_file :
116+ banner_res = subprocess .run (
117+ ['ssh' , f'root@{ hostname_or_ip } ' ] + opts + ['-E' , banner_log_file .name ] + ['\n ' ],
118+ stdout = subprocess .PIPE ,
119+ stderr = subprocess .STDOUT ,
120+ check = False
121+ )
122+ banner_ssherr = banner_log_file .read ()
123+ if banner_res .returncode == 255 :
124+ return SSHCommandFailed (255 , "SSH Error: %s" % banner_ssherr , cmd , ssherr = banner_ssherr )
116125
117- logging .debug (f"[{ hostname_or_ip } ] { cmd } " )
118- process = subprocess .Popen (
119- ssh_cmd ,
120- stdout = subprocess .PIPE ,
121- stderr = subprocess .STDOUT
122- )
123126 if background :
127+ ssh_cmd = ['ssh' , f'root@{ hostname_or_ip } ' ] + opts + [cmd ]
128+ logging .debug (f"[{ hostname_or_ip } ] { cmd } " )
129+ subprocess .Popen (
130+ ssh_cmd ,
131+ stdout = subprocess .PIPE ,
132+ stderr = subprocess .STDOUT
133+ )
124134 return None
125135
126- stdout = []
127- assert process .stdout is not None
128- for line in iter (process .stdout .readline , b'' ):
129- readable_line = line .decode (errors = 'replace' ).strip ()
130- stdout .append (line )
131- logging .debug ("> %s" , readable_line )
132- _ , stderr = process .communicate ()
133- res = subprocess .CompletedProcess (ssh_cmd , process .returncode , b'' .join (stdout ), stderr )
136+ with tempfile .NamedTemporaryFile (suffix = '.log' , prefix = 'ssh_err_' , mode = 'r' ) as ssh_log_file :
137+ opts += ['-E' , ssh_log_file .name ]
138+ ssh_cmd = ['ssh' , f'root@{ hostname_or_ip } ' ] + opts + [cmd ]
139+
140+ logging .debug (f"[{ hostname_or_ip } ] { cmd } " )
141+ process = subprocess .Popen (
142+ ssh_cmd ,
143+ stdout = subprocess .PIPE ,
144+ stderr = subprocess .STDOUT
145+ )
146+
147+ stdout = []
148+ assert process .stdout is not None
149+ for line in iter (process .stdout .readline , b'' ):
150+ readable_line = line .decode (errors = 'replace' ).strip ()
151+ stdout .append (line )
152+ logging .debug ("> %s" , readable_line )
153+ _ , stderr = process .communicate ()
154+ res = subprocess .CompletedProcess (ssh_cmd , process .returncode , b'' .join (stdout ), stderr )
155+
156+ ssherr = ssh_log_file .read ()
157+
158+ if ssherr :
159+ logging .debug ("[%s] ssh stderr: %s" , hostname_or_ip , ssherr )
134160
135161 # Get a decoded version of the output in any case, replacing potential errors
136162 output_for_errors = res .stdout .decode (errors = 'replace' ).strip ()
137163
138164 # Even if check is False, we still raise in case of return code 255, which means a SSH error.
139165 if res .returncode == 255 :
140- return SSHCommandFailed (255 , "SSH Error: %s" % output_for_errors , cmd )
166+ return SSHCommandFailed (255 , "SSH Error: %s" % ssherr , cmd , ssherr = ssherr )
141167
142168 output : bytes = res .stdout
143169 if banner_res :
144- if banner_res .returncode == 255 :
145- return SSHCommandFailed (255 , "SSH Error: %s" % banner_res .stdout .decode (errors = 'replace' ), cmd )
146170 output = output [len (banner_res .stdout ):]
147171
148172 if res .returncode and check :
149- return SSHCommandFailed (res .returncode , output_for_errors , cmd )
173+ return SSHCommandFailed (res .returncode , output_for_errors , cmd , ssherr = ssherr )
150174
151175 if decode :
152176 output_str = output .decode ()
153177 if simple_output :
154178 return output_str .strip ()
155179 else :
156- return SSHResult [str ](res .returncode , output_str )
180+ return SSHResult [str ](res .returncode , output_str , ssherr = ssherr )
157181 else :
158182 if simple_output :
159183 return output .strip ()
160184 else :
161- return SSHResult [bytes ](res .returncode , output )
185+ return SSHResult [bytes ](res .returncode , output , ssherr = ssherr )
162186
163187# The actual code is in _ssh().
164188# This function is kept short for shorter pytest traces upon SSH failures, which are common,
0 commit comments