|  |  |  |  | 

SSH Tutorial for Python and how to connect to Cisco devices

SSH is the industry standard for connecting to a network device. This is unsurprising, given that the legacy protocol, telnet, only allows for insecure remote management. Rather than that, SSH stands for Secure Shell for a reason: it is a truly secure protocol that incorporates all modern security features. As a result, we cannot create SDN software without incorporating SSH. We will demonstrate how to use SSH in a Python program to connect to and control remote devices in this Python SSH Tutorial.

We will demonstrate how to do things correctly in this article.

Are you in a hurry? Simply scroll down for the TL;DR version.

Learn how to use SSH in Python SDN to conenct to remote devices.
SSH encrypts traffic between you and the server, end-to-end.

Read More: Python Multithreading Tutorial: Everything You Need to Know

Before we start about SSH Tutorial for Python and how to connect to Cisco devices

Before we begin the Python SSH Tutorial, we’ll need a mechanism to validate that our program works properly. You are already set if you have a device that allows SSH connections. If you don’t, we have a solution for you. As shown in this post, the first step is to configure a basic GNS3 lab utilizing a single router. After the router has been configured and allocated an IP address, it must be configured for SSH. The commands are as follows.

hostname 
ip domain-name 
crypto key generate rsa
ip ssh version 2

username  privilege 15 secret 

line vty 0 15
 transport input ssh
 login local

Note that the crypto key generate rsa command will ask you the size of your RSA keys. The default is 512, but write 1024, as 512 is not long enough to support SSHv2. The commands we actually typed into our router were the following ones.

hostname R1
ip domain-name sdncore.local
crypto key generate rsa
username admin privilege 15 secret cisco
line vty 0 15
transport input ssh
login local
Screen Shot 2021 06 25 at 1.57.50 PM SSH Tutorial for Python and how to connect to Cisco devices SSH Tutorial for Python and how to connect to Cisco devices

Python SSH Tutorial

Introducing Paramiko

If you followed our python telnet tutorial, you will know that python comes with a pre-installed telnet library. That’s not the case with SSH, so we need to rely on third-party libraries. Currently, one of the best alternatives out there is Paramiko. This library is available for Python 2.7 and Python 3.4 and above and it implements SSHv2 – the current industry standard.  You can even read and contribute to the Paramiko code on GitHub, as it is open source.

To work with Paramiko in our project, we need to install it. Simply enough, we can use this command.

pip install paramiko

This will install Paramiko, as well as any dependency it might have. Now we are finally ready to start with our Python SSH Tutorial.

Why to write an SSH Driver

Our ultimate objective here is to connect to and communicate with a distant device. Thus, SSH is just the tool we utilize to do this. As a consequence, we want the code that executes commands and understands the output to be completely independent of the SSH implementation.

Our ultimate aim is to establish a connection to a distant device. SSH is obnoxious.

Even if you are not working on an SDN project and are only following along with this Python SSH Tutorial, you should adhere to this strategy. This is the only method to ensure that your code continues to grow and scales to meet future needs.

Getting started with the driver

Writing a driver means you have a piece of code that imposes requirements for the code you are trying to write, the driver itself. We already have this piece of code that defines the requirement, it is our driver class. You can check its source code here.

Action required! Create a Python file and name it ssh.py. This is where the SSH driver we are writing should reside. If you are following the sdncore project, you should put it into vty > drivers folder. If not, don’t worry, but there is one thing you need to do: create driver.py. Then, copy the code of our driver class (link above) inside of it. Remember that driver.py and ssh.py must be in the same folder!

hq720 SSH Tutorial for Python and how to connect to Cisco devices SSH Tutorial for Python and how to connect to Cisco devices

Read More: Python Multithreading Tutorial: Everything You Need to Know

Writing an SSH Driver with Python

Starting the file

The first step is to confirm that you have access to all the libraries required for your file. Thus, begin by including the following libraries:

  • The Paramiko client enables you to connect to devices through SSH and the AuthenticationException enables you to detect authentication errors.
  • Threading and Time enable us to do timeout functions, such as reading all text until the timeout expires. It also introduces regular expressions, which are necessary for the expect function.
  • The driver enables you to build your SSH Driver in a consistent manner, allowing you to make connections independent of the underlying protocol in the future.
  • DriverError enables you to alert other classes if an error occurs during the connection process.

To do all of that, you can use this code at the beginning of your ssh.py file.

from paramiko import client
from paramiko.ssh_exception import AuthenticationException
import threading
import time
import re
from .driver import Driver, DriverError

The SSHDriver class

We are going to name our class for SSH connections SSHDriver. We can then create our constructor. It will accept the IP address or domain name of our target device, and username and password for the connection. We also define the default port for SSH connections, which is 22.

All of these parameters were standard parameters from the Driver class. However, we have a new parameter specific for SSH: auto_add_keys. SSH relies on RSA keys, and your PC must know the key of the remote device before allowing the SSH connections. If set to True, we tell our PC to automatically learn the SSH key from the remote device and allow the connection.

class SSHDriver(Driver):
def __init__(self, target, username=, password=, port=22, auto_add_keys=True):
self.target = target
self.username = username
self.password = password
self.port = port
self._client = client.SSHClient()
if auto_add_keys:
self._client.set_missing_host_key_policy(client.AutoAddPolicy())
self._streams = {
‘in’: None,
‘out’: None,
‘err’: None
}

Note that we are also defining the _streams attributes. We will use it to process the output stream coming from the device.

The open() and close() methods

When we init the SSHDriver class, we just prepare it for the connections. However, the connection starts in the moment we call the open() method. This will launch the Paramiko client connection. We also need to catch any error it may raise.

We also want to notify the user if the authentication failed. Furthermore, setting look_for_keys to False should make the authentication process easier. In fact, by doing so, the two devices do not need to know each other’s key beforehand.

When attempting the connection, if we didn’t set auto_add_keys to True and then we connect to a host with unknown keys, we will receive an SSHException. The open() method will convert it into a DriverError, giving some more details about the target host that created the problem.

def open(self):
try:
self._client.connect(self.target,
port=self.port,
username=self.username,
password=self.password,
look_for_keys=False)
except AuthenticationException:
raise DriverError(“Authentication failed”)
except client.SSHException:
raise DriverError(“Keys error when attempting connection to “ + self.target)

Then, we also need to define the close() method that terminates the connection.

def close(self):
self._client.close()

Sending text over SSH with Python

Our Driver abstract class imposes us to implement a send_text function. The role of this function is extremely simple, but we need to take care of the output. The role of this function is just to send the command, and we need to store the variables that handle the output for later usage.

We also want to raise an error in case something is wrong with our command. That is, in case we are unable to send it. So, here is the code.

def send_text(self, text):
try:
self._streams[‘in’], self._streams[‘out’], self._streams[‘err’] = self._client.exec_command(text)
except AttributeError:
raise DriverError(‘Attempting to use a session that was not opened’)

Read Everything

With telnet, we could simply read all the content. Not so easy with SSH. Here we need to verify that the remote device has sent or is sending something, and receive it byte by byte. On top of that, we need to handle the timeout process ourselves. To do that, we need to create a _set_timeout method that is going to be static.

This method is simple. It sleeps for a given time, in seconds, and then set an event. If you are not familiar with the threading jargon, don’t worry. This simply means that after a while it sets a flag. In other parts of our code, we are going to check that flag and as soon as it is set, we are going to interrupt the execution.

@staticmethod
def _set_timeout(event, duration):
time.sleep(duration)
event.set()

We can then use it in our read_eof function. This function inits a set of raw bytes to zero (this is why the b at the beginning) in the variable data. It then creates an Event and launches the _set_timeout function as a thread on that event.

While the thread is sleeping before exiting, we start manipulating our streams with two while loops. We first check that the server has an exit status ready, and then we keep checking if there are data available to read. If they are, we read 1024 bytes and store them into the data variable. We repeat until we finish the data (then the server will not be ready anymore). Not that at the same time, we are also checking if the signal_timeout is set. If so, we immediately interrupt the execution and return what is left.

def read_eof(self, timeout=2):
data = b
signal_timeout = threading.Event()
t1 = threading.Thread(
target=self._set_timeout,
args=(signal_timeout, timeout,)
)
t1.start()
while not signal_timeout.is_set() and not self._streams[‘out’].channel.exit_status_ready():
while not signal_timeout.is_set() and self._streams[‘out’].channel.recv_ready():
data += self._streams[‘out’].channel.recv(1024)
return data

The read_until() function

Our goal in this Python SSH Tutorial is to reflect the behavior of our telnet library. Thus, we need to implement the read_until() function as our Driver class imposes.

This works exactly like the read_eof function, but with a single difference. Every time, we check if we have found a given string in the data. If we did, we simply exit the function with the data we received, including the specified text.

def read_until(self, text, timeout=2):
data = b
signal_timeout = threading.Event()
t1 = threading.Thread(
target=self._set_timeout,
args=(signal_timeout, timeout,)
)
t1.start()
while not signal_timeout.is_set() and not self._streams[‘out’].channel.exit_status_ready():
while not signal_timeout.is_set() and self._streams[‘out’].channel.recv_ready():
data += self._streams[‘out’].channel.recv(1024)
pos = data.find(bytes(text, ‘utf8’))
if pos > -1:
return data[:pos+len(text)]
return data

The except() function

Probably, the except function is the most complex of our entire Python SSH Tutorial. In fact, it involves regular expression matching on the output stream coming from the device.

Don’t let that scare you. In reality, this is simpler than we think. First, the timeout may be None in this function, and this means we don’t need to set the event mechanism to interrupt the function all the time. A simple if check on the timeout will do.

Then, we need to check if the user gave us a list of strings or of regular expressions. In the first case, we must convert the strings into compiled regular expressions.

After that, the function is similar to read_until, but with a key difference. Instead of finding a string, we are matching a regular expression, and doing that for multiple expressions. We need to use a for loop, but the most important part is that we read 1 byte at a time. This is because regular expression matching is not as simple as finding a string, so we want to return just the data that we should return. To achieve that, we need to not overshoot by reading more data than we need.

Finally, we return the regular expression number that matched, the match object and the data read so far.

def expect(self, expr_list, timeout=2):
if timeout is not None:
data = b
signal_timeout = threading.Event()
t1 = threading.Thread(
target=self._set_timeout,
args=(signal_timeout, timeout,)
)
t1.start()
if isinstance(expr_list[0], type(re.compile())):
expressions = expr_list
else:
expressions = []
for expr in expr_list:
expressions.append(re.compile(expr))
while not signal_timeout.is_set() and not self._streams[‘out’].channel.exit_status_ready():
while not signal_timeout.is_set() and self._streams[‘out’].channel.recv_ready():
data += self._streams[‘out’].channel.recv(1)
for i, expr in enumerate(expressions):
result = expr.match(str(data, ‘utf8’))
if result is not None:
return i, result, data
return -1, None, data
Capture SSH Tutorial for Python and how to connect to Cisco devices SSH Tutorial for Python and how to connect to Cisco devices

Python SSH Tutorial TL;DR

Too Long, Didn’t Read? No problem. Here are the key takeaways that will allow you to implement SSH in your Python code, even if you haven’t followed the entire python SSH Tutorial.

  • Install Paramiko with pip install paramiko, it is a library for handling SSH connections
  • First, create a paramiko.client.SSHClient object, the constructor doesn’t want any parameter
  • Then, call connect() on this object. You want to specify the target host as first parameter, and then use named parameters for port, username and password.
  • To avoid getting jammed in key validation, you can set the look_for_keys parameters of connect() to False, and also use set_missing_host_key_policy(paramiko.client.AutoAddPolicy()) on the client.
  • Sending a command is easy, use exec_command(cmd) on the client, it returns a tuple with standard in, standard out and standard error streams (in this order).
  • To read from such streams, take a look at the snippet below. It is a function that expect the standard output received after executing a command.
def read(stdout):
data = b
while stdout.channel.exit_status_ready():
while stdout.channel.recv_ready():
data += stdout.channel.recv(1024)
return data

Read More: Python Multithreading Tutorial: Everything You Need to Know

Conclusion

We demonstrated how to utilize SSH in Python with Paramiko and how to develop a driver that is functionally comparable to the one we had for Telnet in this post. If you’re working on remotely managing devices, this will be really beneficial since you’ll be able to control them regardless of the protocol they use. To do so, just read the following articles on ICTShore.com. (Tip: subscribe to the email to ensure you get them as soon as they are released!)

What are your thoughts on SSH? Are you still utilizing telnet in some capacity and contemplating a migration? Kindly inform me of your thoughts on this SDN project. Additionally, don’t forget to peruse the GitHub page.

If Our Method Resolve Your Problem Consider To Share This Post, You can help more People Facing This Problem and also, if you want, you can Subscribe at Our Youtube Channel as Well!