Skip to content

Commit 4a8adf0

Browse files
ssh: Use paramiko instead of pexpect
1 parent 745dc94 commit 4a8adf0

File tree

2 files changed

+89
-162
lines changed

2 files changed

+89
-162
lines changed

devlib/utils/ssh.py

Lines changed: 88 additions & 162 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@
2929
from pipes import quote
3030
from future.utils import raise_from
3131

32+
from paramiko.client import SSHClient, AutoAddPolicy
33+
from paramiko.ssh_exception import NoValidConnectionsError
34+
# By default paramiko is very verbose, including at the INFO level
35+
logging.getLogger("paramiko").setLevel(logging.WARNING)
36+
3237
# pylint: disable=import-error,wrong-import-position,ungrouped-imports,wrong-import-order
3338
import pexpect
3439
from distutils.version import StrictVersion as V
@@ -54,15 +59,7 @@
5459
logger = logging.getLogger('ssh')
5560
gem5_logger = logging.getLogger('gem5-connection')
5661

57-
def ssh_get_shell(host,
58-
username,
59-
password=None,
60-
keyfile=None,
61-
port=None,
62-
timeout=10,
63-
telnet=False,
64-
original_prompt=None,
65-
options=None):
62+
def ssh_get_shell(host, username, password=None, keyfile=None, port=None, timeout=10, telnet=False, original_prompt=None):
6663
_check_env()
6764
start_time = time.time()
6865
while True:
@@ -71,8 +68,7 @@ def ssh_get_shell(host,
7168
raise ValueError('keyfile may not be used with a telnet connection.')
7269
conn = TelnetPxssh(original_prompt=original_prompt)
7370
else: # ssh
74-
conn = pxssh.pxssh(options=options,
75-
echo=False)
71+
conn = pxssh.pxssh(echo=False)
7672

7773
try:
7874
if keyfile:
@@ -159,7 +155,6 @@ def check_keyfile(keyfile):
159155

160156
class SshConnection(object):
161157

162-
default_password_prompt = '[sudo] password'
163158
max_cancel_attempts = 5
164159
default_timeout = 10
165160

@@ -171,7 +166,7 @@ def name(self):
171166
def connected_as_root(self):
172167
if self._connected_as_root is None:
173168
# Execute directly to prevent deadlocking of connection
174-
result = self._execute_and_wait_for_prompt('id', as_root=False)
169+
exit_code, result = self._execute('id', as_root=False)
175170
self._connected_as_root = 'uid=0(' in result
176171
return self._connected_as_root
177172

@@ -187,12 +182,8 @@ def __init__(self,
187182
keyfile=None,
188183
port=None,
189184
timeout=None,
190-
telnet=False,
191-
password_prompt=None,
192-
original_prompt=None,
193185
platform=None,
194-
sudo_cmd="sudo -- sh -c {}",
195-
options=None
186+
sudo_cmd="sudo -S -- sh -c {}"
196187
):
197188
self._connected_as_root = None
198189
self.host = host
@@ -201,185 +192,120 @@ def __init__(self,
201192
self.keyfile = check_keyfile(keyfile) if keyfile else keyfile
202193
self.port = port
203194
self.lock = threading.Lock()
204-
self.password_prompt = password_prompt if password_prompt is not None else self.default_password_prompt
205195
self.sudo_cmd = sanitize_cmd_template(sudo_cmd)
206196
logger.debug('Logging in {}@{}'.format(username, host))
207197
timeout = timeout if timeout is not None else self.default_timeout
208-
self.options = options if options is not None else {}
209-
self.conn = ssh_get_shell(host,
210-
username,
211-
password,
212-
self.keyfile,
213-
port,
214-
timeout,
215-
False,
216-
None,
217-
self.options)
198+
199+
client = SSHClient()
200+
client.load_system_host_keys()
201+
client.set_missing_host_key_policy(AutoAddPolicy)
202+
client.connect(
203+
hostname=host,
204+
port=port,
205+
username=username,
206+
password=password,
207+
key_filename=keyfile,
208+
timeout=timeout,
209+
)
210+
self.client = client
211+
self.chan = self._get_channel()
218212
atexit.register(self.close)
219213

214+
def _get_channel(self):
215+
transport = self.client.get_transport()
216+
channel = transport.open_session()
217+
return channel
218+
219+
def _get_sftp(self, timeout):
220+
sftp = self.client.open_sftp()
221+
sftp.get_channel().settimeout(timeout)
222+
return sftp
223+
220224
def push(self, source, dest, timeout=30):
221-
dest = '{}@{}:{}'.format(self.username, self.host, dest)
222-
return self._scp(source, dest, timeout)
225+
sftp = self._get_sftp(timeout)
226+
sftp.put(source, dest)
223227

224228
def pull(self, source, dest, timeout=30):
225-
source = '{}@{}:{}'.format(self.username, self.host, source)
226-
return self._scp(source, dest, timeout)
229+
sftp = self._get_sftp(timeout)
230+
sftp.get(source, dest)
227231

228232
def execute(self, command, timeout=None, check_exit_code=True,
229233
as_root=False, strip_colors=True, will_succeed=False): #pylint: disable=unused-argument
230234
if command == '':
231-
# Empty command is valid but the __devlib_ec stuff below will
232-
# produce a syntax error with bash. Treat as a special case.
233235
return ''
234236
try:
237+
_command = '({}) 2>&1'.format(command)
235238
with self.lock:
236-
_command = '({}); __devlib_ec=$?; echo; echo $__devlib_ec'.format(command)
237-
full_output = self._execute_and_wait_for_prompt(_command, timeout, as_root, strip_colors)
238-
split_output = full_output.rsplit('\r\n', 2)
239-
try:
240-
output, exit_code_text, _ = split_output
241-
except ValueError as e:
242-
raise TargetStableError(
243-
"cannot split reply (target misconfiguration?):\n'{}'".format(full_output))
244-
if check_exit_code:
245-
try:
246-
exit_code = int(exit_code_text)
247-
if exit_code:
248-
message = 'Got exit code {}\nfrom: {}\nOUTPUT: {}'
249-
raise TargetStableError(message.format(exit_code, command, output))
250-
except (ValueError, IndexError):
251-
logger.warning(
252-
'Could not get exit code for "{}",\ngot: "{}"'\
253-
.format(command, exit_code_text))
254-
return output
255-
except EOF:
239+
exit_code, output = self._execute(_command, timeout, as_root, strip_colors)
240+
except NoValidConnectionsError:
256241
raise TargetNotRespondingError('Connection lost.')
257242
except TargetStableError as e:
258243
if will_succeed:
259244
raise TargetTransientError(e)
260245
else:
261246
raise
247+
else:
248+
if check_exit_code and exit_code:
249+
message = 'Got exit code {}\nfrom: {}\nOUTPUT: {}'
250+
raise TargetStableError(message.format(exit_code, command, output))
251+
return output
262252

263253
def background(self, command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, as_root=False):
254+
channel = self._get_channel()
264255
try:
265-
port_string = '-p {}'.format(self.port) if self.port else ''
266-
keyfile_string = '-i {}'.format(self.keyfile) if self.keyfile else ''
267-
if as_root and not self.connected_as_root:
268-
command = self.sudo_cmd.format(command)
269-
options = " ".join([ "-o {}={}".format(key,val)
270-
for key,val in self.options.items()])
271-
command = '{} {} {} {} {}@{} {}'.format(ssh,
272-
options,
273-
keyfile_string,
274-
port_string,
275-
self.username,
276-
self.host,
277-
command)
278-
logger.debug(command)
279-
if self.password:
280-
command, _ = _give_password(self.password, command)
281-
return subprocess.Popen(command, stdout=stdout, stderr=stderr, shell=True)
282-
except EOF:
256+
return channel.exec_command(command)
257+
except NoValidConnectionsError:
283258
raise TargetNotRespondingError('Connection lost.')
284259

285260
def close(self):
286261
logger.debug('Logging out {}@{}'.format(self.username, self.host))
287262
try:
288-
self.conn.logout()
289-
except:
263+
self.client.close()
264+
except NoValidConnectionsError:
290265
logger.debug('Connection lost.')
291-
self.conn.close(force=True)
292-
293-
def cancel_running_command(self):
294-
# simulate impatiently hitting ^C until command prompt appears
295-
logger.debug('Sending ^C')
296-
for _ in range(self.max_cancel_attempts):
297-
self._sendline(chr(3))
298-
if self.conn.prompt(0.1):
299-
return True
300-
return False
301-
302-
def _execute_and_wait_for_prompt(self, command, timeout=None, as_root=False, strip_colors=True, log=True):
303-
self.conn.prompt(0.1) # clear an existing prompt if there is one.
304-
if as_root and self.connected_as_root:
305-
# As we're already root, there is no need to use sudo.
306-
as_root = False
307-
if as_root:
308-
command = self.sudo_cmd.format(quote(command))
309-
if log:
310-
logger.debug(command)
311-
self._sendline(command)
312-
if self.password:
313-
index = self.conn.expect_exact([self.password_prompt, TIMEOUT], timeout=0.5)
314-
if index == 0:
315-
self._sendline(self.password)
316-
else: # not as_root
317-
if log:
318-
logger.debug(command)
319-
self._sendline(command)
320-
timed_out = self._wait_for_prompt(timeout)
321-
if sys.version_info[0] == 3:
322-
output = process_backspaces(self.conn.before.decode(sys.stdout.encoding or 'utf-8', 'replace'))
323-
else:
324-
output = process_backspaces(self.conn.before)
325266

326-
if timed_out:
327-
self.cancel_running_command()
328-
raise TimeoutError(command, output)
267+
def _execute(self, command, timeout=None, as_root=False, strip_colors=True, log=True):
268+
# As we're already root, there is no need to use sudo.
269+
use_sudo = not (as_root and self.connected_as_root)
270+
log_debug = logger.debug if log else lambda msg: None
271+
272+
if use_sudo and not self.password:
273+
raise TargetStableError('Attempt to use sudo but no password was specified')
274+
275+
try:
276+
if use_sudo:
277+
command = self.sudo_cmd.format(quote(command))
278+
log_debug(command)
279+
stdin, stdout, stderr = self.client.exec_command(command, timeout=timeout)
280+
stdin.write(self.password + '\n')
281+
stdin.flush()
282+
else:
283+
log_debug(command)
284+
stdin, stdout, stderr = self.client.exec_command(command, timeout=timeout)
285+
except socket.timeout:
286+
raise TimeoutError(command)
287+
288+
# Empty the stdout buffer of the command, allowing it to carry on to
289+
# completion
290+
output_chunks = []
291+
chunk = True
292+
while chunk:
293+
chunk = stdout.read()
294+
output_chunks.append(chunk)
295+
296+
# Wait until the command completes
297+
exit_code = stdout.channel.recv_exit_status()
298+
299+
# Join in one go to avoid O(N^2) concatenation
300+
output = b''.join(output_chunks)
301+
302+
if sys.version_info[0] == 3:
303+
output = output.decode(sys.stdout.encoding or 'utf-8', 'replace')
329304
if strip_colors:
330305
output = strip_bash_colors(output)
331-
return output
332306

333-
def _wait_for_prompt(self, timeout=None):
334-
if timeout:
335-
return not self.conn.prompt(timeout)
336-
else: # cannot timeout; wait forever
337-
while not self.conn.prompt(1):
338-
pass
339-
return False
340-
341-
def _scp(self, source, dest, timeout=30):
342-
# NOTE: the version of scp in Ubuntu 12.04 occasionally (and bizarrely)
343-
# fails to connect to a device if port is explicitly specified using -P
344-
# option, even if it is the default port, 22. To minimize this problem,
345-
# only specify -P for scp if the port is *not* the default.
346-
port_string = '-P {}'.format(quote(str(self.port))) if (self.port and self.port != 22) else ''
347-
keyfile_string = '-i {}'.format(quote(self.keyfile)) if self.keyfile else ''
348-
options = " ".join(["-o {}={}".format(key,val)
349-
for key,val in self.options.items()])
350-
command = '{} {} -r {} {} {} {}'.format(scp,
351-
options,
352-
keyfile_string,
353-
port_string,
354-
quote(source),
355-
quote(dest))
356-
command_redacted = command
357-
logger.debug(command)
358-
if self.password:
359-
command, command_redacted = _give_password(self.password, command)
360-
try:
361-
check_output(command, timeout=timeout, shell=True)
362-
except subprocess.CalledProcessError as e:
363-
raise_from(HostError("Failed to copy file with '{}'. Output:\n{}".format(
364-
command_redacted, e.output)), None)
365-
except TimeoutError as e:
366-
raise TimeoutError(command_redacted, e.output)
367-
368-
def _sendline(self, command):
369-
# Workaround for https://github.com/pexpect/pexpect/issues/552
370-
if len(command) == self._get_window_size()[1] - self._get_prompt_length():
371-
command += ' '
372-
self.conn.sendline(command)
373-
374-
@memoized
375-
def _get_prompt_length(self):
376-
self.conn.sendline()
377-
self.conn.prompt()
378-
return len(self.conn.after)
307+
return (exit_code, output)
379308

380-
@memoized
381-
def _get_window_size(self):
382-
return self.conn.getwinsize()
383309

384310
class TelnetConnection(SshConnection):
385311

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@
8282
'python-dateutil', # converting between UTC and local time.
8383
'pexpect>=3.3', # Send/recieve to/from device
8484
'pyserial', # Serial port interface
85+
'paramiko', # SSH connection
8586
'wrapt', # Basic for construction of decorator functions
8687
'future', # Python 2-3 compatibility
8788
'enum34;python_version<"3.4"', # Enums for Python < 3.4

0 commit comments

Comments
 (0)