I recently decided to use paramiko to develop a remote command execution tool.
It was very easy to setup initially and ran much faster than my existing pexpect implementation but it had a problem with sudo commands because they required the password to be provided as input.
I solved the problem by using a pseudo-terminal and by creating my own ChannelFile objects for stdin and stdout/stderr. The solution should be general enough to handle any case that requires simple input but it is not as flexible as pexpect. I hope that you find it useful.
Current Version
This is similar to the older version below but it handles more than 64KiB by eliminating the stdout buffer, the stdin buffer and using recv polling. It even does a silly check for input. That check could be expanded to make it behave more like pexpect but that is for another day.
#!/usr/bin/env python
'''
This class allows you to run commands on a remote host and provide
input if necessary.
VERSION 1.2
'''
import paramiko
import logging
import socket
import time
import datetime
# ================================================================
# class MySSH
# ================================================================
class MySSH:
'''
Create an SSH connection to a server and execute commands.
Here is a typical usage:
ssh = MySSH()
ssh.connect('host', 'user', 'password', port=22)
if ssh.connected() is False:
sys.exit('Connection failed')
# Run a command that does not require input.
status, output = ssh.run('uname -a')
print 'status = %d' % (status)
print 'output (%d):' % (len(output))
print '%s' % (output)
# Run a command that does requires input.
status, output = ssh.run('sudo uname -a', 'sudo-password')
print 'status = %d' % (status)
print 'output (%d):' % (len(output))
print '%s' % (output)
'''
def __init__(self, compress=True, verbose=False):
'''
Setup the initial verbosity level and the logger.
@param compress Enable/disable compression.
@param verbose Enable/disable verbose messages.
'''
self.ssh = None
self.transport = None
self.compress = compress
self.bufsize = 65536
# Setup the logger
self.logger = logging.getLogger('MySSH')
self.set_verbosity(verbose)
fmt = '%(asctime)s MySSH:%(funcName)s:%(lineno)d %(message)s'
format = logging.Formatter(fmt)
handler = logging.StreamHandler()
handler.setFormatter(format)
self.logger.addHandler(handler)
self.info = self.logger.info
def __del__(self):
if self.transport is not None:
self.transport.close()
self.transport = None
def connect(self, hostname, username, password, port=22):
'''
Connect to the host.
@param hostname The hostname.
@param username The username.
@param password The password.
@param port The port (default=22).
@returns True if the connection succeeded or false otherwise.
'''
self.info('connecting %s@%s:%d' % (username, hostname, port))
self.hostname = hostname
self.username = username
self.port = port
self.ssh = paramiko.SSHClient()
self.ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
try:
self.ssh.connect(hostname=hostname,
port=port,
username=username,
password=password)
self.transport = self.ssh.get_transport()
self.transport.use_compression(self.compress)
self.info('succeeded: %s@%s:%d' % (username,
hostname,
port))
except socket.error as e:
self.transport = None
self.info('failed: %s@%s:%d: %s' % (username,
hostname,
port,
str(e)))
except paramiko.BadAuthenticationType as e:
self.transport = None
self.info('failed: %s@%s:%d: %s' % (username,
hostname,
port,
str(e)))
return self.transport is not None
def run(self, cmd, input_data=None, timeout=10):
'''
Run a command with optional input data.
Here is an example that shows how to run commands with no input:
ssh = MySSH()
ssh.connect('host', 'user', 'password')
status, output = ssh.run('uname -a')
status, output = ssh.run('uptime')
Here is an example that shows how to run commands that require input:
ssh = MySSH()
ssh.connect('host', 'user', 'password')
status, output = ssh.run('sudo uname -a', '<sudo-password>')
@param cmd The command to run.
@param input_data The input data (default is None).
@param timeout The timeout in seconds (default is 10 seconds).
@returns The status and the output (stdout and stderr combined).
'''
self.info('running command: (%d) %s' % (timeout, cmd))
if self.transport is None:
self.info('no connection to %s@%s:%s' % (str(self.username),
str(self.hostname),
str(self.port)))
return -1, 'ERROR: connection not established\n'
# Fix the input data.
input_data = self._run_fix_input_data(input_data)
# Initialize the session.
self.info('initializing the session')
session = self.transport.open_session()
session.set_combine_stderr(True)
session.get_pty()
session.exec_command(cmd)
output = self._run_poll(session, timeout, input_data)
status = session.recv_exit_status()
self.info('output size %d' % (len(output)))
self.info('status %d' % (status))
return status, output
def connected(self):
'''
Am I connected to a host?
@returns True if connected or false otherwise.
'''
return self.transport is not None
def set_verbosity(self, verbose):
'''
Turn verbose messages on or off.
@param verbose Enable/disable verbose messages.
'''
if verbose > 0:
self.logger.setLevel(logging.INFO)
else:
self.logger.setLevel(logging.ERROR)
def _run_fix_input_data(self, input_data):
'''
Fix the input data supplied by the user for a command.
@param input_data The input data (default is None).
@returns the fixed input data.
'''
if input_data is not None:
if len(input_data) > 0:
if '\\n' in input_data:
# Convert \n in the input into new lines.
lines = input_data.split('\\n')
input_data = '\n'.join(lines)
return input_data.split('\n')
return []
def _run_send_input(self, session, stdin, input_data):
'''
Send the input data.
@param session The session.
@param stdin The stdin stream for the session.
@param input_data The input data (default is None).
'''
if input_data is not None:
self.info('session.exit_status_ready() %s' % str(session.exit_status_ready()))
self.info('stdin.channel.closed %s' % str(stdin.channel.closed))
if stdin.channel.closed is False:
self.info('sending input data')
stdin.write(input_data)
def _run_poll(self, session, timeout, input_data):
'''
Poll until the command completes.
@param session The session.
@param timeout The timeout in seconds.
@param input_data The input data.
@returns the output
'''
interval = 0.1
maxseconds = timeout
maxcount = maxseconds / interval
# Poll until completion or timeout
# Note that we cannot directly use the stdout file descriptor
# because it stalls at 64K bytes (65536).
input_idx = 0
timeout_flag = False
self.info('polling (%d, %d)' % (maxseconds, maxcount))
start = datetime.datetime.now()
start_secs = time.mktime(start.timetuple())
output = ''
session.setblocking(0)
while True:
if session.recv_ready():
data = session.recv(self.bufsize)
output += data
self.info('read %d bytes, total %d' % (len(data), len(output)))
if session.send_ready():
# We received a potential prompt.
# In the future this could be made to work more like
# pexpect with pattern matching.
if input_idx < len(input_data):
data = input_data[input_idx] + '\n'
input_idx += 1
self.info('sending input data %d' % (len(data)))
session.send(data)
self.info('session.exit_status_ready() = %s' % (str(session.exit_status_ready())))
if session.exit_status_ready():
break
# Timeout check
now = datetime.datetime.now()
now_secs = time.mktime(now.timetuple())
et_secs = now_secs - start_secs
self.info('timeout check %d %d' % (et_secs, maxseconds))
if et_secs > maxseconds:
self.info('polling finished - timeout')
timeout_flag = True
break
time.sleep(0.200)
self.info('polling loop ended')
if session.recv_ready():
data = session.recv(self.bufsize)
output += data
self.info('read %d bytes, total %d' % (len(data), len(output)))
self.info('polling finished - %d output bytes' % (len(output)))
if timeout_flag:
self.info('appending timeout message')
output += '\nERROR: timeout after %d seconds\n' % (timeout)
session.close()
return output
# ================================================================
# MAIN
# ================================================================
if __name__ == '__main__':
import sys
# Access variables.
hostname = 'hostname'
port = 22
username = 'username'
password = 'password'
sudo_password = password # assume that it is the same password
# Create the SSH connection
ssh = MySSH()
ssh.set_verbosity(False)
ssh.connect(hostname=hostname,
username=username,
password=password,
port=port)
if ssh.connected() is False:
print 'ERROR: connection failed.'
sys.exit(1)
def run_cmd(cmd, indata=None):
'''
Run a command with optional input.
@param cmd The command to execute.
@param indata The input data.
@returns The command exit status and output.
Stdout and stderr are combined.
'''
print
print '=' * 64
print 'command: %s' % (cmd)
status, output = ssh.run(cmd, indata)
print 'status : %d' % (status)
print 'output : %d bytes' % (len(output))
print '=' * 64
print '%s' % (output)
run_cmd('uname -a')
run_cmd('sudo ls -ltrh /var/log | tail', sudo_password) # sudo command
Older Version
This version will not handle more than 64KiB of output. I am not sure why.
#!/usr/bin/env python
'''
This class allows you to run commands on a remote host and provide
input if necessary.
'''
import paramiko
import logging
import socket
import time
# ================================================================
# class MySSH
# ================================================================
class MySSH:
'''
Create an SSH connection to a server and execute commands.
Here is a typical usage:
ssh = MySSH()
ssh.connect('host', 'user', 'password', port=22)
if ssh.connected() is False:
sys.exit('Connection failed')
# Run a command that does not require input.
status, output = ssh.run('uname -a')
print 'status = %d' % (status)
print 'output (%d):' % (len(output))
print '%s' % (output)
# Run a command that does requires input.
status, output = ssh.run('sudo uname -a', 'sudo-password')
print 'status = %d' % (status)
print 'output (%d):' % (len(output))
print '%s' % (output)
'''
def __init__(self, compress=True, verbose=False):
'''
Setup the initial verbosity level and the logger.
@param compress Enable/disable compression.
@param verbose Enable/disable verbose messages.
'''
self.ssh = None
self.transport = None
self.compress = compress
# Setup the logger
self.logger = logging.getLogger('MySSH')
self.set_verbosity(verbose)
fmt = '%(asctime)s MySSH:%(funcName)s:%(lineno)d %(message)s'
format = logging.Formatter(fmt)
handler = logging.StreamHandler()
handler.setFormatter(format)
self.logger.addHandler(handler)
self.info = self.logger.info
def __del__(self):
if self.transport is not None:
self.transport.close()
self.transport = None
def connect(self, hostname, username, password, port=22):
'''
Connect to the host.
@param hostname The hostname.
@param username The username.
@param password The password.
@param port The port (default=22).
@returns True if the connection succeeded or false otherwise.
'''
self.info('connecting %s@%s:%d' % (username, hostname, port))
self.hostname = hostname
self.username = username
self.port = port
self.ssh = paramiko.SSHClient()
self.ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
try:
self.ssh.connect(hostname=hostname,
port=port,
username=username,
password=password)
self.transport = self.ssh.get_transport()
self.transport.use_compression(self.compress)
self.info('succeeded: %s@%s:%d' % (username,
hostname,
port))
except socket.error:
self.transport = None
self.info('failed: %s@%s:%d: %s' % (username,
hostname,
port,
str(e)))
except paramiko.BadAuthenticationType as e:
self.transport = None
self.info('failed: %s@%s:%d: %s' % (username,
hostname,
port,
str(e)))
return self.transport is not None
def run(self, cmd, input_data=None, timeout=10):
'''
Run a command with optional input data.
Here is an example that shows how to run commands with no input:
ssh = MySSH()
ssh.connect('host', 'user', 'password')
status, output = ssh.run('uname -a')
status, output = ssh.run('uptime')
Here is an example that shows how to run commands that require input:
ssh = MySSH()
ssh.connect('host', 'user', 'password')
status, output = ssh.run('sudo uname -a', '<sudo-password>')
@param cmd The command to run.
@param input_data The input data (default is None).
@param timeout The timeout in seconds (default is 10 seconds).
@returns The status and the output (stdout and stderr combined).
'''
self.info('running command: (%d) %s' % (timeout, cmd))
if self.transport is None:
self.info('no connection to %s@%s:%s' % (str(self.username),
str(self.hostname),
str(self.port)))
return -1, 'ERROR: connection not established\n'
# Fix the input data.
input_data = self._run_fix_input_data(input_data)
# Initialize the session.
self.info('initializing the session')
session = self.transport.open_session()
session.set_combine_stderr(True)
session.get_pty()
session.exec_command(cmd)
stdin = session.makefile('wb', -1)
stdout = session.makefile('rb', -1)
self._run_send_input(stdout, stdin, input_data)
output = self._run_poll(stdout, timeout, input_data)
status = stdout.channel.recv_exit_status()
self.info('output size %d' % (len(output)))
self.info('status %d' % (status))
return status, output
def connected(self):
'''
Am I connected to a host?
@returns True if connected or false otherwise.
'''
return self.transport is not None
def set_verbosity(self, verbose):
'''
Turn verbose messages on or off.
@param verbose Enable/disable verbose messages.
'''
if verbose is True:
self.logger.setLevel(logging.INFO)
else:
self.logger.setLevel(logging.ERROR)
def _run_fix_input_data(self, input_data):
'''
Fix the input data supplied by the user for a command.
@param input_data The input data (default is None).
@returns the fixed input data.
'''
if input_data is not None:
if len(input_data) > 0:
if '\\n' in input_data:
# Convert \n in the input into new lines.
lines = input_data.split('\\n')
input_data = '\n'.join(lines)
if input_data[-1] != '\n':
input_data += '\n'
return input_data
def _run_send_input(self, stdout, stdin, input_data):
'''
Send the input data.
@param stdout The stdout stream for the session.
@param stdin The stdin stream for the session.
@param input_data The input data (default is None).
'''
if stdout.channel.closed is False:
if input_data is not None:
self.info('sending input data')
stdin.write(input_data)
def _run_poll(self, stdout, timeout, input_data):
'''
Poll until the command completes.
@param timeout The timeout in seconds.
@param input_data The input data.
@returns the output
'''
interval = 0.1
maxseconds = timeout
maxcount = maxseconds / interval
# Poll until completion or timeout
self.info('polling (%d, %d)' % (maxseconds, maxcount))
count = 0
while stdout.channel.closed is False and count <= maxcount:
count += 1
time.sleep(interval)
# Polling finished.
if stdout.channel.closed is False:
# Some sort of error occurred, assume a timeout.
self.info('polling finished - timeout')
stdout.channel.close()
output = stdout.read()
output += '\nERROR: timeout after %d seconds\n' % (timeout)
else:
output = stdout.read()
self.info('polling finished - %d output bytes' % (len(output)))
if input_data is not None:
# Strip out the input data.
output = output[len(input_data):]
self.info('stripped %d input bytes' % (len(input_data)))
return output
# ================================================================
# MAIN
# ================================================================
if __name__ == '__main__':
import sys
# Access variables.
hostname = 'hostname'
port = 22
username = 'username'
password = 'password'
sudo_password = password # assume that it is the same password
# Create the SSH connection
ssh = MySSH()
ssh.set_verbosity(False)
ssh.connect(hostname=hostname,
username=username,
password=password,
port=port)
if ssh.connected() is False:
print 'ERROR: connection failed.'
sys.exit(1)
def run_cmd(cmd, indata=None):
'''
Run a command with optional input.
@param cmd The command to execute.
@param indata The input data.
@returns The command exit status and output.
Stdout and stderr are combined.
'''
print
print '=' * 64
print 'command: %s' % (cmd)
status, output = ssh.run(cmd, indata)
print 'status : %d' % (status)
print 'output : %d bytes' % (len(output))
print '=' * 64
print '%s' % (output)
run_cmd('uname -a')
run_cmd('sudo ls -ltrh /var/log | tail', sudo_password) # sudo command
Updated to a newer version that correctly supports more than 64KiB of output.
Updated the current version again to correct a blocking problem when input was expected. Also fixed a timeout problem.
Thanks jlinoff for the valuable code.
Srikanth
Excelent !
Hello Joe!
Thanks a lot for you code.
I was debugging my code for the last two days untill I ran into your page…
My code was missing the .get_pty() call
Anyhow, thanks a lot for your help.
I tried to run your script from a windows machine but it does not seem to work when using commands that require interactivity.
It is not clear from your description what is happening. How does it fail? Are you using a cygwin environment? Which version of windows are you using?
Hi Joe!
Great post! I am new to development and have been spending considerable time understanding the instantiation of classes and functions in your script to tune it for my needs.
Had a couple questions:
session.send(data) on line 242, how is that working? I understand it pushes the sudo password but couldn’t find a method “send” that was executing this?
How is _run_send_input being used? I don’t see it referenced anywhere?
What I am doing with this:
I am trying to tune this and combine it with peexpect potentially to login to a Linux System that is connected to a Fortinet FW CLI and issue certain commands recording the output
Feel free to suggest best ways to do this and very much appreciate the write up and reading of my questions above!
Hi Joe,
Thanks for the script. I used your script it works fine but at the last I get timeout error. Even if i set timeout to 100 it wait till that time and gives timeout error.
following is the output of the script:
Resolving deltas: 98% (8714/8889)
Resolving deltas: 99% (8806/8889)
Resolving deltas: 100% (8889/8889)
Resolving deltas: 100% (8889/8889), done.
ERROR: timeout after 100 seconds
I am tryign to clone a repo on a remote machine. AFter the above output it check for the connection and then print done but the script is not waiting for that line and it always timeout. I increased the timeout to very high value but then also it will give same error. this is not time -out error but its failing to read the last lines and giving this error.
please sugges.
thanks,
Madhusudan
Hey,
I solved the issue by setting timeout flag to False. 🙂
If this code can read multiple inputs from user?
I am not sure that i understand the question. Can you provide an example?