11import base64
22import logging
3- import shlex
43import subprocess
54
65import lib .config as config
7- from lib .common import HostAddress
86
97from typing import List , Literal , Union , overload
108
@@ -17,14 +15,6 @@ def __init__(self, returncode, stdout, cmd, exception_msg):
1715 self .stdout = stdout
1816 self .cmd = cmd
1917
20- class SSHCommandFailed (BaseCommandFailed ):
21- def __init__ (self , returncode , stdout , cmd ):
22- msg_end = f": { stdout } " if stdout else "."
23- super (SSHCommandFailed , self ).__init__ (
24- returncode , stdout , cmd ,
25- f'SSH command ({ cmd } ) failed with return code { returncode } { msg_end } '
26- )
27-
2818class LocalCommandFailed (BaseCommandFailed ):
2919 def __init__ (self , returncode , stderr , cmd ):
3020 msg_end = f": { stderr } " if stderr else "."
@@ -40,10 +30,6 @@ def __init__(self, returncode, stdout):
4030 self .returncode = returncode
4131 self .stdout = stdout
4232
43- class SSHResult (BaseCmdResult ):
44- def __init__ (self , returncode , stdout ):
45- super (SSHResult , self ).__init__ (returncode , stdout )
46-
4733class LocalCommandResult (BaseCmdResult ):
4834 def __init__ (self , returncode , stdout , stderr ):
4935 super (LocalCommandResult , self ).__init__ (returncode , stdout )
@@ -62,191 +48,6 @@ def _ellide_log_lines(log):
6248 reduced_message .append ("(...)" )
6349 return "\n {}" .format ("\n " .join (reduced_message ))
6450
65- def _ssh (hostname_or_ip , cmd , check , simple_output , suppress_fingerprint_warnings ,
66- background , decode , options ) -> Union [SSHResult , SSHCommandFailed , str , bytes , None ]:
67- opts = list (options )
68- opts .append ('-o "BatchMode yes"' )
69- if suppress_fingerprint_warnings :
70- # Suppress warnings and questions related to host key fingerprints
71- # because on a test network IPs get reused, VMs are reinstalled, etc.
72- # Based on https://unix.stackexchange.com/a/365976/257493
73- opts .append ('-o "StrictHostKeyChecking no"' )
74- opts .append ('-o "LogLevel ERROR"' )
75- opts .append ('-o "UserKnownHostsFile /dev/null"' )
76-
77- if isinstance (cmd , str ):
78- command = cmd
79- else :
80- command = " " .join (cmd )
81-
82- ssh_cmd = f"ssh root@{ hostname_or_ip } { ' ' .join (opts )} { shlex .quote (command )} "
83-
84- # Fetch banner and remove it to avoid stdout/stderr pollution.
85- banner_res = None
86- if config .ignore_ssh_banner :
87- banner_res = subprocess .run (
88- "ssh root@%s %s '%s'" % (hostname_or_ip , ' ' .join (opts ), '\n ' ),
89- shell = True ,
90- stdout = subprocess .PIPE ,
91- stderr = subprocess .STDOUT ,
92- check = False
93- )
94-
95- logging .debug (f"[{ hostname_or_ip } ] { command } " )
96- process = subprocess .Popen (
97- ssh_cmd ,
98- shell = True ,
99- stdout = subprocess .PIPE ,
100- stderr = subprocess .STDOUT
101- )
102- if background :
103- return None
104-
105- stdout = []
106- assert process .stdout is not None
107- for line in iter (process .stdout .readline , b'' ):
108- readable_line = line .decode (errors = 'replace' ).strip ()
109- stdout .append (line )
110- logging .debug ("> %s" , readable_line )
111- _ , stderr = process .communicate ()
112- res = subprocess .CompletedProcess (ssh_cmd , process .returncode , b'' .join (stdout ), stderr )
113-
114- # Get a decoded version of the output in any case, replacing potential errors
115- output_for_errors = res .stdout .decode (errors = 'replace' ).strip ()
116-
117- # Even if check is False, we still raise in case of return code 255, which means a SSH error.
118- if res .returncode == 255 :
119- return SSHCommandFailed (255 , "SSH Error: %s" % output_for_errors , command )
120-
121- output : Union [bytes , str ] = res .stdout
122- if banner_res :
123- if banner_res .returncode == 255 :
124- return SSHCommandFailed (255 , "SSH Error: %s" % banner_res .stdout .decode (errors = 'replace' ), command )
125- output = output [len (banner_res .stdout ):]
126-
127- if decode :
128- assert isinstance (output , bytes )
129- output = output .decode ()
130-
131- if res .returncode and check :
132- return SSHCommandFailed (res .returncode , output_for_errors , command )
133-
134- if simple_output :
135- return output .strip ()
136- return SSHResult (res .returncode , output )
137-
138- # The actual code is in _ssh().
139- # This function is kept short for shorter pytest traces upon SSH failures, which are common,
140- # as pytest prints the whole function definition that raised the SSHCommandFailed exception
141- @overload
142- def ssh (hostname_or_ip : HostAddress , cmd : Union [str , List [str ]], * , check : bool = True ,
143- simple_output : Literal [True ] = True ,
144- suppress_fingerprint_warnings : bool = True , background : Literal [False ] = False ,
145- decode : Literal [True ] = True , options : List [str ] = []) -> str :
146- ...
147- @overload
148- def ssh (hostname_or_ip : HostAddress , cmd : Union [str , List [str ]], * , check : bool = True ,
149- simple_output : Literal [True ] = True ,
150- suppress_fingerprint_warnings : bool = True , background : Literal [False ] = False ,
151- decode : Literal [False ], options : List [str ] = []) -> bytes :
152- ...
153- @overload
154- def ssh (hostname_or_ip : HostAddress , cmd : Union [str , List [str ]], * , check : bool = True ,
155- simple_output : Literal [False ],
156- suppress_fingerprint_warnings : bool = True , background : Literal [False ] = False ,
157- decode : bool = True , options : List [str ] = []) -> SSHResult :
158- ...
159- @overload
160- def ssh (hostname_or_ip : HostAddress , cmd : Union [str , List [str ]], * , check : bool = True ,
161- simple_output : Literal [False ],
162- suppress_fingerprint_warnings : bool = True , background : Literal [True ],
163- decode : bool = True , options : List [str ] = []) -> None :
164- ...
165- @overload
166- def ssh (hostname_or_ip : HostAddress , cmd : Union [str , List [str ]], * , check = True ,
167- simple_output : bool = True ,
168- suppress_fingerprint_warnings = True , background : bool = False ,
169- decode : bool = True , options : List [str ] = []) \
170- -> Union [str , bytes , SSHResult , None ]:
171- ...
172- def ssh (hostname_or_ip , cmd , * , check = True , simple_output = True ,
173- suppress_fingerprint_warnings = True ,
174- background = False , decode = True , options = []):
175- result_or_exc = _ssh (hostname_or_ip , cmd , check , simple_output , suppress_fingerprint_warnings ,
176- background , decode , options )
177- if isinstance (result_or_exc , SSHCommandFailed ):
178- raise result_or_exc
179- else :
180- return result_or_exc
181-
182- def ssh_with_result (hostname_or_ip , cmd , suppress_fingerprint_warnings = True ,
183- background = False , decode = True , options = []) -> SSHResult :
184- result_or_exc = _ssh (hostname_or_ip , cmd , False , False , suppress_fingerprint_warnings ,
185- background , decode , options )
186- if isinstance (result_or_exc , SSHCommandFailed ):
187- raise result_or_exc
188- elif isinstance (result_or_exc , SSHResult ):
189- return result_or_exc
190- assert False , "unexpected type"
191-
192- def scp (hostname_or_ip , src , dest , check = True , suppress_fingerprint_warnings = True , local_dest = False ):
193- # local import to avoid cyclic import; lib.netutils also import lib.commands
194- from lib .netutil import wrap_ip
195-
196- opts = '-o "BatchMode yes"'
197- if suppress_fingerprint_warnings :
198- # Suppress warnings and questions related to host key fingerprints
199- # because on a test network IPs get reused, VMs are reinstalled, etc.
200- # Based on https://unix.stackexchange.com/a/365976/257493
201- opts = '-o "StrictHostKeyChecking no" -o "LogLevel ERROR" -o "UserKnownHostsFile /dev/null"'
202-
203- ip = wrap_ip (hostname_or_ip )
204- if local_dest :
205- src = 'root@{}:{}' .format (ip , src )
206- else :
207- dest = 'root@{}:{}' .format (ip , dest )
208-
209- command = "scp {} {} {}" .format (opts , src , dest )
210- res = subprocess .run (
211- command ,
212- shell = True ,
213- stdout = subprocess .PIPE ,
214- stderr = subprocess .STDOUT ,
215- check = False
216- )
217-
218- errorcode_msg = "" if res .returncode == 0 else " - Got error code: %s" % res .returncode
219- logging .debug (f"[{ hostname_or_ip } ] scp: { src } => { dest } { errorcode_msg } " )
220-
221- if check and res .returncode :
222- raise SSHCommandFailed (res .returncode , res .stdout .decode (), command )
223-
224- return res
225-
226- def sftp (hostname_or_ip , cmds , check = True , suppress_fingerprint_warnings = True ):
227- opts = ''
228- if suppress_fingerprint_warnings :
229- # Suppress warnings and questions related to host key fingerprints
230- # because on a test network IPs get reused, VMs are reinstalled, etc.
231- # Based on https://unix.stackexchange.com/a/365976/257493
232- opts = '-o "StrictHostKeyChecking no" -o "LogLevel ERROR" -o "UserKnownHostsFile /dev/null"'
233-
234- args = "sftp {} -b - root@{}" .format (opts , hostname_or_ip )
235- input = bytes ("\n " .join (cmds ), 'utf-8' )
236- res = subprocess .run (
237- args ,
238- input = input ,
239- shell = True ,
240- stdout = subprocess .PIPE ,
241- stderr = subprocess .STDOUT ,
242- check = False
243- )
244-
245- if check and res .returncode :
246- raise SSHCommandFailed (res .returncode , res .stdout .decode (), "{} -- {}" .format (args , cmds ))
247-
248- return res
249-
25051@overload
25152def local_cmd (cmd : Union [str , List [str ]], * , check : bool = True , simple_output : Literal [True ] = True ,
25253 decode : Literal [True ] = True ) -> str :
0 commit comments