11
11
import time
12
12
13
13
import attr
14
+ from pexpect .fdpexpect import fdspawn
15
+ from pexpect .exceptions import EOF , TIMEOUT
14
16
15
17
from ..factory import target_factory
16
18
from ..protocol import CommandProtocol , FileTransferProtocol
22
24
from ..util .proxy import proxymanager
23
25
from ..util .timeout import Timeout
24
26
from ..util .ssh import get_ssh_connect_timeout
27
+ from ..util .marker import gen_marker
25
28
26
29
27
30
@target_factory .reg_driver
@@ -34,6 +37,9 @@ class SSHDriver(CommandMixin, Driver, CommandProtocol, FileTransferProtocol):
34
37
stderr_merge = attr .ib (default = False , validator = attr .validators .instance_of (bool ))
35
38
connection_timeout = attr .ib (default = float (get_ssh_connect_timeout ()), validator = attr .validators .instance_of (float ))
36
39
explicit_sftp_mode = attr .ib (default = False , validator = attr .validators .instance_of (bool ))
40
+ su_password = attr .ib (default = None , validator = attr .validators .optional (attr .validators .instance_of (str )))
41
+ su_username = attr .ib (default = "root" , validator = attr .validators .instance_of (str ))
42
+ su_prompt = attr .ib (default = "Password:" , validator = attr .validators .instance_of (str ))
37
43
38
44
def __attrs_post_init__ (self ):
39
45
super ().__attrs_post_init__ ()
@@ -180,6 +186,40 @@ def _start_own_master_once(self, timeout):
180
186
def run (self , cmd , codec = "utf-8" , decodeerrors = "strict" , timeout = None ):
181
187
return self ._run (cmd , codec = codec , decodeerrors = decodeerrors , timeout = timeout )
182
188
189
+ def handle_password (self , fd , stdin , marker ):
190
+ p = fdspawn (fd , timeout = 15 )
191
+ try :
192
+ p .expect ([f"{ marker } \n " ])
193
+ except TIMEOUT :
194
+ raise ExecutionError (f"Failed to find marker before su: { p .buffer !r} " )
195
+ except EOF :
196
+ raise ExecutionError ("Unexpected disconnect before su" )
197
+
198
+ try :
199
+ index = p .expect ([f"{ marker } \n " , self .su_prompt ])
200
+ except TIMEOUT :
201
+ raise ExecutionError (f"Unexpected output from su: { p .buffer !r} " )
202
+ except EOF :
203
+ raise ExecutionError ("Unexpected disconnect after starting su" )
204
+
205
+ if index == 0 :
206
+ # no password needed
207
+ return p .after
208
+
209
+ stdin .write (f"{ self .su_password } " .encode ("utf-8" ))
210
+ # It seems we need to close stdin here to reliably get su to accept the
211
+ # password. \n doesn't seem to work.
212
+ stdin .close ()
213
+
214
+ try :
215
+ p .expect ([f"{ marker } \n " ])
216
+ except TIMEOUT :
217
+ raise ExecutionError (f"Unexpected output from su after entering password: { p .buffer !r} " )
218
+ except EOF :
219
+ raise ExecutionError (f"Unexpected disconnect after after entering su password: { p .before !r} " )
220
+
221
+ return p .after
222
+
183
223
def _run (self , cmd , codec = "utf-8" , decodeerrors = "strict" , timeout = None ):
184
224
"""Execute `cmd` on the target.
185
225
@@ -196,22 +236,48 @@ def _run(self, cmd, codec="utf-8", decodeerrors="strict", timeout=None):
196
236
complete_cmd = ["ssh" , "-x" , * self .ssh_prefix ,
197
237
"-p" , str (self .networkservice .port ), "-l" , self .networkservice .username ,
198
238
self .networkservice .address
199
- ] + cmd .split (" " )
239
+ ]
240
+ if self .su_password :
241
+ self .stderr_merge = True # with -tt, we get all output on stdout
242
+ marker = gen_marker ()
243
+ complete_cmd += ["-tt" , "--" , "echo" , f"{ marker } ;" , "su" , self .su_username , "--" ]
244
+ inner_cmd = f"echo '{ marker [:4 ]} ''{ marker [4 :]} '; { cmd } "
245
+ complete_cmd += ["-c" , shlex .quote (inner_cmd )]
246
+ else :
247
+ complete_cmd += ["--" ] + cmd .split (" " )
248
+
200
249
self .logger .debug ("Sending command: %s" , complete_cmd )
201
250
if self .stderr_merge :
202
251
stderr_pipe = subprocess .STDOUT
203
252
else :
204
253
stderr_pipe = subprocess .PIPE
254
+ stdin = subprocess .PIPE if self .su_password else None
255
+ stdout , stderr = b"" , b""
205
256
try :
206
257
sub = subprocess .Popen (
207
- complete_cmd , stdout = subprocess .PIPE , stderr = stderr_pipe
258
+ complete_cmd , stdout = subprocess .PIPE , stderr = stderr_pipe , stdin = stdin ,
208
259
)
209
260
except :
210
261
raise ExecutionError (
211
262
f"error executing command: { complete_cmd } "
212
263
)
213
264
214
- stdout , stderr = sub .communicate (timeout = timeout )
265
+ if self .su_password :
266
+ fd = sub .stdout if self .stderr_merge else sub .stderr
267
+ output = self .handle_password (fd , sub .stdin , marker )
268
+ sub .stdin .close ()
269
+ self .logger .debug (f"su leftover output: %s" , output )
270
+ if self .stderr_merge :
271
+ stderr += output
272
+ else :
273
+ stdout += output
274
+
275
+ sub .stdin = None # never try to write to stdin here
276
+ comout , comerr = sub .communicate (timeout = timeout )
277
+ stdout += comout
278
+ if not self .stderr_merge :
279
+ stderr += comerr
280
+
215
281
stdout = stdout .decode (codec , decodeerrors ).split ('\n ' )
216
282
if stdout [- 1 ] == '' :
217
283
stdout .pop ()
0 commit comments