Python package – “ipaddress”

IP Address (Internet Protocol) is a fundamental networking concept that provides address assignation capability in a network. The python module ipaddress is used extensively to validate and categorize IP address to IPV4 and IPV6 type. It can also be used to do comparison of the IP address values as well as IP address arithmetic for manipulating the ip addresses.

Validating the entered input IP address

import ipaddress

IP_ADDRESS = input("Enter IP(IPv4/IPv6) Address : ")
try:
    network = ipaddress.ip_address(IP_ADDRESS)
except ValueError:
    print('Entered IP Address is invalid:', IP_ADDRESS)
else:
    print('Entered IP Address is valid:' + IP_ADDRESS)
    print(network)
$ python ip_address.py
Entered IP Address is valid: 1.1.1.1
1.1.1.1/32

$ python ip_address.py
Entered IP Address is invalid:1.1.1.256

The ip_address function validates the IPv4 address. If the range of values is beyond 0 to 255, then it throws an error i.e. “ValueError”.

Validating the entered Subnet and its details

The program takes an IP address and a subnet mask as input and gives the following information about the inputs:

  • Validating Subnet
  • Number of available hosts.
  • Network Address
  • Broadcast Address
  • Number of valid hosts per subnet
# Validating the entered subnet & Calculating multiple parameters associated in a Given Subnet
IP_SUBNET = input("Enter IP(IPv4/IPv6) subnet : ")
try:
    network = ipaddress.ip_network(IP_SUBNET)
except ValueError:
    print('Entered IP Address is invalid:', IP_SUBNET)
else:
    print('Entered IP Address is valid:' + IP_SUBNET)
    print('Number of Hosts available in this Subnet' + IP_SUBNET + ':' + str(network.num_addresses))
    print('Host List: ' + str(list(network.hosts())))
    print('Network Address: ' + str(network.network_address))
    print('Broadcast Address: ' + str(network.broadcast_address))
    print('Subnet Mask: ' + str(network.netmask))
    print('Subnet Mask Bit : ' + str(network.prefixlen))
$ python ip_address.py
Enter IP(IPv4/IPv6) subnet : 192.168.1.0/29
Entered IP Address is valid:192.168.1.0/29
Number of Hosts available in this Subnet192.168.1.0/29:8
Host List: [IPv4Address('192.168.1.1'), IPv4Address('192.168.1.2'), IPv4Address('192.168.1.3'), IPv4Address('192.168.1.4'), IPv4Address('192.168.1.5'), IPv4A
ddress('192.168.1.6')]
Network Address: 192.168.1.0
Broadcast Address: 192.168.1.7
Subnet Mask: 255.255.255.248
Subnet Mask Bit : 29

$ python ip_address.py
Enter IP(IPv4/IPv6) subnet : 192.168.1.1/29
Entered IP Address is invalid: 192.168.1.1/29

$ python ip_address.py
Enter IP(IPv4/IPv6) subnet : 192.168.1.0 /29
Entered IP Address is invalid: 192.168.1.0 /29

Last two did not execute because subnet entered was “192.168.1.1/29” which is actually not representing subnet and is actually a host. In the latter, we have actually added an space.

# Lets understand the purpose of "ipaddress" package
# Primary Goals
#       1. Validating IP Address/IP Subnet
#       2. Determining Host Address, Network Address, Broadcast Address
# Our objective is to create a Python program name "ip_address" which accept these inputs
#       1. IP_ADDRESS :
#       2. IP_SUBNET :

import ipaddress

# Validating entered IP Address
IP_ADDRESS = input("Enter IP(IPv4/IPv6) Address : ")
try:
    network = ipaddress.ip_address(IP_ADDRESS)
except ValueError:
    print('Entered IP Address is invalid:', IP_ADDRESS)
else:
    print('Entered IP Address is valid:' + IP_ADDRESS)

# Validating the entered subnet & Calculating multiple parameters associated in a Given Subnet
IP_SUBNET = input("Enter IP(IPv4/IPv6) subnet : ")
try:
    network = ipaddress.ip_network(IP_SUBNET)
except ValueError:
    print('Entered IP Address is invalid:', IP_SUBNET)
else:
    print('Entered IP Address is valid:' + IP_SUBNET)
    print('Number of Hosts available in this Subnet' + IP_SUBNET + ':' + str(network.num_addresses))
    print('Host List: ' + str(list(network.hosts())))
    print('Network Address: ' + str(network.network_address))
    print('Broadcast Address: ' + str(network.broadcast_address))
    print('Subnet Mask: ' + str(network.netmask))
    print('Subnet Mask Bit : ' + str(network.prefixlen))

Python package – “paramiko”

Module “Paramiko” is a python implementation of SSH v2 i.e. if we have any requirement of accessing a host via SSH; This module must be used.

We primarily use “paramiko” for login into the devices and run some commands.

Connecting to a Remote Host

Refer to below code snippet

import paramiko
DEVICE_IP = '10.120.235.166'
USERNAME = 'admin'
PASSWORD = 'Nvidia@557'

# Lets create an Object SSH
SSH = paramiko.SSHClient()

try:
SSH.connect(DEVICE_IP,port=22,username=USERNAME,password=PASSWORD)
except paramiko.SSHException:
print('SSH ERROR', paramiko.SSHException)
else:
print('SSH is successful to device ', + DEVICE_IP)

If we execute this; we will get an error as below

Traceback (most recent call last):
  File "C:\ProgramData\Anaconda3\lib\site-packages\IPython\core\interactiveshell.py", line 3296, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)
  File "<ipython-input-5-27d441bf6606>", line 1, in <module>
    SSH.connect(DEVICE_IP,port=22,username=USERNAME,password=PASSWORD)
  File "C:\ProgramData\Anaconda3\lib\site-packages\paramiko\client.py", line 416, in connect
    self, server_hostkey_name, server_key
  File "C:\ProgramData\Anaconda3\lib\site-packages\paramiko\client.py", line 824, in missing_host_key
    "Server {!r} not found in known_hosts".format(hostname)
paramiko.ssh_exception.SSHException: Server '10.120.235.166' not found in known_hosts

We get an error; It is basically an exception that says that “server_key” is not found in known_hosts because it is missing in missing_host_key. There may be the instances where we might not want to trust the host and python default behavior is to Reject.
In “paramiko” there are actually two policies
1. paramiko “Reject” policy – Default one as shown above
2. paramiko “Auto-add” policy – Automatically accepting all and added them to the Host file.

Lets override the default policy behavior with “Auto-add” policy as shown below

import paramiko
DEVICE_IP = '10.120.235.166'
USERNAME = 'admin'
PASSWORD = 'Nvidia@557'

# Lets create an Object SSH
SSH = paramiko.SSHClient()

# Lets override the default policy behavior
SSH.set_missing_host_key_policy(paramiko.AutoAddPolicy())

try:
SSH.connect(DEVICE_IP,port=22,username=USERNAME,password=PASSWORD)
except paramiko.SSHException:
print('SSH ERROR', paramiko.SSHException)
else:
print('SSH is successful to device ' + DEVICE_IP)

Refer for output

$ python demo_paramiko.py
SSH is successful to device 10.120.235.166

You can observe that our connection is successful which can also be seen from device terminal

sw1-server.ban-in#show user
    Line       User       Host(s)              Idle       Location
*  2 vty 0     gaagrawal  idle                 00:00:00 10.24.71.57

Let’s add more complexity i.e. we want to track following scenario in SSH connection.

  1. Authentication failed due to wrong Username/Password
  2. Network Connectivity issue
  3. SSH timeout

Refer to this snipped to track above real-time issues

import paramiko
import socket
DEVICE_IP = '10.10.10.10'
USERNAME = 'gaagrawal'
PASSWORD = 'Kanika!88'

# Lets create an Object SSH
SSH = paramiko.SSHClient()

# Lets override the default policy behavior
SSH.set_missing_host_key_policy(paramiko.AutoAddPolicy())

try:
SSH.connect(DEVICE_IP,port=22,username=USERNAME,password=PASSWORD)
except paramiko.AuthenticationException:
print("Authentication failed, please verify your credential")
except paramiko.SSHException:
print("Could not establish SSH connection: %s" % paramiko.SSHException)
except Exception as TimeoutError:
print("Unable to connect, please verify network connectivity")
except socket.timeout as e:
print("Connection got timed out")
else:
print('SSH is successful to device ' + DEVICE_IP)

Executing some commands on Remote Host

Now, we are connected to the remote server. The next step is to execute commands on the SSH server. To run a command on the server the exec_command() function is called on the SSHClient with the command passed as input. When you execute commands using exec_command a new Channel is opened and the requested command is executed. The response is returned as Python file-like objects representing stdin, stdout, and stderr(as a 3-tuple)

  • The stdin is a write-only file which can be used for input commands.
  • The stdout file give the output of the command.
  • The stderr gives the errors returned on executing the command. Will be empty if there is no error.
import paramiko
import socket
DEVICE_IP = '10.24.3.4'
USERNAME = 'gaagrawal'
PASSWORD = 'Kanika!88'
COMMAND = 'show etherchannel summary'
# Lets create an Object SSH
SSH = paramiko.SSHClient()

# Lets override the default policy behavior
SSH.set_missing_host_key_policy(paramiko.AutoAddPolicy())

try:
SSH.connect(DEVICE_IP,port=22,username=USERNAME,password=PASSWORD)
except paramiko.AuthenticationException:
print("Authentication failed, please verify your credential")
except paramiko.SSHException:
print("Could not establish SSH connection: %s" % paramiko.SSHException)
except Exception as TimeoutError:
print("Unable to connect, please verify network connectivity")
except socket.timeout as e:
print("Connection got timed out")
else:
print('SSH is successful to device ' + DEVICE_IP)
stdin, stdout, stderr = SSH.exec_command(COMMAND, timeout=10)
SSH.ssh_output = stdout.readlines()
SSH.ssh_error = stderr.readlines()
if SSH.ssh_error:
print("Problem occurred while running command:" + COMMAND + " The error is " + SSH.ssh_error)
else:
print("Command execution completed successfully")
print('\n'.join(SSH.ssh_output))

Closing SSH connection

As a best practice; once our Job is done with SSH connection – It is advised to close the connection. This can be achieved using “SSH.close()”

Python Package – “argparse”

Hello all,

Today, we will discuss our first package in Python Series for Network Engineers i.e. “argparse”.

Primary questions answered are

  • Why to Use it? – The argparse module makes it easy to write user-friendly command-line interfaces.
  • Why I like to use it so much?

To demonstrate this, We will begin with a simple program (not simple though if it is your first python program) for DNS look up. As all of us are aware, DNS converts “IP Address” to “FQDN (Fully Qualified Domain Name)” and vice versa.

This is how my setup looks like

  1. Domain name: example.com
  2. DNS server (dns @ 192.168.100.100)
  3. Two Hosts (pc2 @ 192.168.1.2 & pc3 @ 192.168.1.3)

Below is the output I received for DNS lookup.

PS C:\Users\gaagrawal> nslookup
Default Server:  dns.example.com
Address:  192.168.100.100

> 192.168.1.2
Server:  dns.example.com
Address:  192.168.100.100

Name:    pc2.example.com
Address:  192.168.1.2

> pc3.example.com
Server:  dns.example.com
Address:  192.168.100.100

Name:    pc3.example.com
Address:  192.168.1.3

In Linux, when you run the “ls” command without any options, it will default displaying the contents of the current directory If you run “ls” on a different directory that you currently are in, you would type “ls directory_name”. The “directory_name” is a “positional argument”, which means that the program know what to do with the value. To get more information about a file we can use the “-l” switch. The “-l” is known as an “optional argument” If you want to display the help text of the ls command, you would type “ls –help”. Hence we are trying to achieve something similar with “argparse”.

Let us start with a program without using “argparse” –

import socket

# Let us demonstrate this without "argparse"
ip_address = '192.168.1.2'
host_name = 'pc3.example.com'

HOSTNAME = socket.gethostbyaddr(ip_address)
ADDRESS = socket.gethostbyname(host_name)
print(HOSTNAME[0], ADDRESS)

Kindly ignore the “socket” package and respective methods at the moment. These will be discussed in more details at the later section. Let’s execute this – we shall receive the corresponding output;

(base) C:\Users\gaagrawal\Desktop\Learn_Python>python dns_lookup_positional.py
pc2.example.com 192.168.1.3

So, the problem with this approach is we have to edit this program every time there is a change in Input which is practically impossible if we have to use this script for thousands of host.

Therefore, It would be great and beneficial – if there was an option to provide such input arguments at the execution of this program. This is all possible with the help of “argparse” package.

Let’s take a look at this program (i.e. demonstrate “argparse” package) –

import socket
import argparse

# Creating a Parser
PARSER = argparse.ArgumentParser(description='Process Command Line')
# The ArgumentParser object will hold all the information 
# necessary to parse the command line into Python data types

# Adding Arguments
PARSER.add_argument('ip_address', type=str, help='IP Address of host')
PARSER.add_argument('host_name', type=str, help='Host Name of host')

# Parsing Arguments
arguments = PARSER.parse_args()

HOSTNAME = socket.gethostbyaddr(arguments.ip_address)
ADDRESS = socket.gethostbyname(arguments.host_name)

print(HOSTNAME[0], ADDRESS)

When we execute this with the given arguments – it works as expected.

$ python dns_lookup_positional.py 192.168.1.2 pc3.example.com
pc2.example.com 192.168.1.3

But again, there is some problem with this? – It is very difficult to understand these positional arguments i.e. we cannot be sure on which Argument should be written first (until and unless you have written this program) i.e. “IP Address” or “Host Name”. What will happen if I swap these input arguments – It will not work. Because, the logic expects “IP Address” as its 1st positional Argument and “Host Name” as  2nd.  

$ python dns_lookup_positional.py pc3.example.com 192.168.1.2
pc3.example.com 192.168.1.2

To verify the order of positional arguments – we can execute this command on our python console.
The --help option, which can also be shortened to -h, is the only option we get for free (i.e. no need to specify it). Specifying anything else results in an error. But even then, we do get a useful usage message, also for free.

$ python dns_lookup_positional.py -h
usage: dns_lookup_positional.py [-h] ip_address host_name

Process Command Line

positional arguments:
  ip_address  IP Address of host
  host_name   Host Name of host

optional arguments:
  -h, --help  show this help message and exit

Hence, positional arguments are not the best way to use in a professional environment. Let’s take a look at the below program which uses optional argument.

import socket
import argparse

# Creating a Parser
PARSER = argparse.ArgumentParser(description='Process Command Line')
# The ArgumentParser object will hold all the information necessary to parse the command line
# into Python data types

# Adding Arguments
# Metavar flag is used just to make --help menu look better
# We have used "-H" instead of "-h" because -h is reserved for Help
PARSER.add_argument('-i','--ip_address', metavar='',type=str, required='TRUE',help='IP Address of host')
PARSER.add_argument('-H','--host_name', metavar='',type=str, required='TRUE',help='Host Name of host')

# Parsing Arguments
arguments = PARSER.parse_args()

HOSTNAME = socket.gethostbyaddr(arguments.ip_address)
ADDRESS = socket.gethostbyname(arguments.host_name)
print(HOSTNAME[0], ADDRESS)

To verify all the changes in this execution – execute as below

$ python dns_lookup_optional.py -h
usage: dns_lookup_optional.py [-h] -i  -H

Process Command Line

optional arguments:
  -h, --help          show this help message and exit
  -i , --ip_address   IP Address of host
  -H , --host_name    Host Name of host

Now, irrespective of where you keep your arguments position. Program will execute and result in same output.

$ python dns_lookup_optional.py -i 192.168.1.2 -H pc3.example.com 
pc2.example.com 192.168.1.3

$ python dns_lookup_optional.py -H pc3.example.com -i 192.168.1.2  
pc2.example.com 192.168.1.3

Hope it clarifies “argparse” package. Refer to complete python programs

# Lets understand the purpose of "argparse" package
# Primary Goals
#       1. Reading Arguments
#       2. Understanding Positional Arguments
# Our objective is to create a Python program name "dns_lookup_positional" which accept two input
#       1. IP ADDRESS
#       2. HOST NAME (FQDN)
# & output must be a Corresponding lookups
# Hence,
#      program name : dns_lookup_positional
# For a moment please forget about the method and library used at the moment;
# These will be discussed in more details in sequential manner

import socket
# Let us demonstrate this without "argparse"
# ip_address = '192.168.1.2'
# host_name = 'pc3.example.com'
#
# HOSTNAME = socket.gethostbyaddr(ip_address)
# ADDRESS = socket.gethostbyname(host_name)
# print(HOSTNAME[0], ADDRESS)

import argparse

# Creating a Parser
PARSER = argparse.ArgumentParser(description='Process Command Line')
# The ArgumentParser object will hold all the information necessary to parse the command line
# into Python data types

# Adding Arguments
PARSER.add_argument('ip_address', type=str, help='IP Address of host')
PARSER.add_argument('host_name', type=str, help='Host Name of host')

# Parsing Arguments
arguments = PARSER.parse_args()

HOSTNAME = socket.gethostbyaddr(arguments.ip_address)
ADDRESS = socket.gethostbyname(arguments.host_name)
print(HOSTNAME[0], ADDRESS)
# Lets understand the purpose of "argparse" package
# Primary Goals
#       1. Reading Arguments
#       2. Understanding Optional Arguments
# Our objective is to create a Python program name "dns_lookup_optional" which accept two input
#       1. IP ADDRESS
#       2. HOST NAME (FQDN)
# & output must results to a Corresponding lookups.
# Hence,
#      program name : dns_lookup_optional
# Let me demonstrate this without using "argparse"
# For a moment please forget about the method and library used at the moment;
# These will be discussed in more details in later section

import socket
import argparse

# Creating a Parser
PARSER = argparse.ArgumentParser(description='Process Command Line')
# The ArgumentParser object will hold all the information necessary to parse the command line
# into Python data types

# Adding Arguments
# Metavar flag is used just to make --help menu look better
# We have used "-H" instead of "-h" because -h is reserved for Help
PARSER.add_argument('-i','--ip_address', metavar='',type=str, required='TRUE',help='IP Address of host')
PARSER.add_argument('-H','--host_name', metavar='',type=str, required='TRUE',help='Host Name of host')

# Parsing Arguments
arguments = PARSER.parse_args()

HOSTNAME = socket.gethostbyaddr(arguments.ip_address)
ADDRESS = socket.gethostbyname(arguments.host_name)
print(HOSTNAME[0], ADDRESS)

Python package – “netmiko” … Part 3

In the previous sections, if you observe carefully, we have been mentioning about the device type while defining connection dictionary. How about imagine a situation where we have a set of network devices from different OEMs and need to execute the same functions without mentioning the device type? – In this section, we will discuss about two important classes which can be used to auto-discover the OS type of device.

  1. SSHDetect
  2. SNMPDetect

To achieve this; we can use the auto-detect functionality of “netmiko” – It uses a combination of SNMP discovery OIDS and executes several show commands on the remote console to detect the router operating system and type, based on the output string. Then netmiko will load the appropriate driver into the ConnectHandler() class:

SSHDetect

from netmiko import ConnectHandler
from netmiko import SSHDetect

# Define a connection Handler i.e. Device dictionary
CISCO_3850 = {
    'device_type': 'autodetect',
    'ip': '10.24.7.3',
    'username': 'admin',
    'password': 'admin',
    'port': '22',          # optional, defaults to 22
    'secret': '',          # optional, defaults to ''
}

DETECT_DEVICE = SSHDetect(**CISCO_3850)
DEVICE_TYPE = DETECT_DEVICE.autodetect()
if DEVICE_TYPE is None:
    print("Error: No auto device detection for specified device.")
    exit()
else:
    print(DEVICE_TYPE)
    print(DETECT_DEVICE.potential_matches)
    CISCO_3850['device_type'] = DEVICE_TYPE

    # Establish an SSH connection to the device by passing in the device dictionary.
    CONNECT = ConnectHandler(**CISCO_3850)

    # Execute show commands.
    OUTPUT = CONNECT.send_command('show run int GigabitEthernet0/0/0')
    print(OUTPUT)

SNMPDetect

from netmiko import ConnectHandler
from netmiko.snmp_autodetect import SNMPDetect
import pysnmp
# Define a connection Handler i.e. Device dictionary
CISCO_3850 = {
    'device_type': 'autodetect',
    'ip': '10.24.7.3',
    'username': 'admin',
    'password': 'admin',
    'port': '22',          # optional, defaults to 22
    'secret': '',          # optional, defaults to ''
}

# SNMP Detection
SNMP_COMMUNITY = 'test'
DETECT_DEVICE = SNMPDetect(CISCO_3850['ip'], snmp_version='v2c', community=SNMP_COMMUNITY)

DEVICE_TYPE = DETECT_DEVICE.autodetect()

if DEVICE_TYPE is None:
    print("Error: No auto device detection for specified device.")
    exit()
else:
    print(DEVICE_TYPE)
    print(DETECT_DEVICE.potential_matches)
    CISCO_3850['device_type'] = DEVICE_TYPE

    # Establish an SSH connection to the device by passing in the device dictionary.
    CONNECT = ConnectHandler(**CISCO_3850)

    # Execute show commands.
    OUTPUT = CONNECT.send_command('show run int GigabitEthernet0/0/0')
    print(OUTPUT)

Let’s execute this –

$ python demo_netmiko_autodetect.py
cisco_ios
{'cisco_ios': 99}
Building configuration...

Current configuration : 99 bytes
!
interface GigabitEthernet0/0/0
 description TEST
 no ip address
 shutdown
 negotiation auto
end

Please note, above program require an additional library to be installed i.e. “pysnmp” which will be discussed later in more details.

Python package – “netmiko” … Part 2

Objective: The primary objective of this Blog is to demonstrate following –

  1. Handling commands which requires further inputs
  2. Importance of “delay_factor”

Handling Interactive commands

Let’s take a case where execute one command may ask for additional information & requires users to interact e.g. If we run below command it will ask for multiple additional parameters to execute further. (IP 192.168.1.2 is representing FTP server)

Router##ping
Protocol [ip]: ip
Target IP address: 192.168.1.2
Repeat count [5]: 5
Datagram size [100]:
Timeout in seconds [2]:
Extended commands [n]:
Sweep range of sizes [n]:
Type escape sequence to abort.
Sending 5, 100-byte ICMP Echos to 192.168.1.2, timeout is 2 seconds:
.....
Success rate is 0 percent (0/5)

So, how do we handle these interactive commands in Python? – One of the way is to use the “expect_string” argument to “send_command()” method. The “expect_string” argument tells Netmiko what to look for in the output. Refer to updated code for this –

OUTPUT = CONNECT.send_command('ping', expect_string=r'Protocol')
OUTPUT = OUTPUT + CONNECT.send_command('ip', expect_string=r'Target IP address')
OUTPUT = OUTPUT + CONNECT.send_command('192.168.1.2', expect_string=r'Repeat count')
OUTPUT = OUTPUT + CONNECT.send_command('\n', expect_string=r'Datagram size')
OUTPUT = OUTPUT + CONNECT.send_command('\n', expect_string=r'Timeout in seconds')
OUTPUT = OUTPUT + CONNECT.send_command('\n', expect_string=r'Extended commands')
OUTPUT = OUTPUT + CONNECT.send_command('\n', expect_string=r'Sweep range of sizes')
try:
    OUTPUT = OUTPUT + CONNECT.send_command('\n', expect_string=r'#')
except Exception:
    EXECUTION = time.time() - start
    print('Total time to execute this code is ' + str(EXECUTION))
    CONNECT.disconnect()
    raise
else:
    EXECUTION = time.time() - start
    print('Total time to execute this code is ' + str(EXECUTION))
    print(OUTPUT)

Here is the output that we receive when we execute this:

$python demo_netmiko_interactive.py
Total time to execute this code is 21.20832872390747
Protocol [ip]: Target IP address: Repeat count [5]: Datagram size [100]: Timeout in seconds [2]: Extended commands [n]: Sweep range of sizes [n]: Type escape
 sequence to abort.
Sending 5, 100-byte ICMP Echos to 192.168.1.2, timeout is 2 seconds:
.....
Success rate is 0 percent (0/5)

Importance of delay_factor

Let’s begin this with a use case of copying a file “test.bin” from Router to an FTP server 192.168.1.1 . To copy this we will use following command on the Router CLI

Router#copy flash:test.bin tftp:
Address or name of remote host []? 192.168.1.2
Destination filename [test.bin]?
!!!.!!!!.!!.!.!!!.!.!.!.!!!
2431050 bytes copied in 262.212 secs (9271 bytes/sec)

Note that this file copy took ~262 seconds to complete. Let’s execute this with the python program and observe the results.

OUTPUT = CONNECT.send_command('copy flash:pp-adv-isr4000-155-3.S2-23-21.0.0.pack tftp:', expect_string=r'Address or name of remote host')
OUTPUT = OUTPUT + CONNECT.send_command('192.168.1.2', expect_string=r'Destination filename')

try:
    OUTPUT = OUTPUT + CONNECT.send_command('\n', expect_string=r'#')
except Exception:
    EXECUTION = time.time() - start
    print('Total time to execute this code is ' + str(EXECUTION))
    CONNECT.disconnect()
    raise
else:
    EXECUTION = time.time() - start
    print('Total time to execute this code is ' + str(EXECUTION))
    print(OUTPUT)
    CONNECT.disconnect()
$python demo_netmiko_delay.py
Total time to execute this code is 110.00358414649963
Traceback (most recent call last):
  File "demo_netmiko_delay.py", line 89, in <module>
    OUTPUT = OUTPUT + CONNECT.send_command('\n', expect_string=r'#')
  File "C:\ProgramData\Anaconda3\lib\site-packages\netmiko\base_connection.py", line 1322, in send_command
    search_pattern
OSError: Search pattern never detected in send_command_expect: #

From this output, we can refer netmiko actually waited for 110 seconds to complete though. So, what’s happening behind the scene? – the default timeout of send_command() is roughly 100 seconds; the additional 10 seconds is overhead time. But our actual file copy requires ~262 seconds to complete and netmiko’s send_command() only waits for 100 sec. Hence, we need a mechanism to modify these timers. To achieve this we can leverage the delay_factor.

delay_factor is an argument that can be pass into method send_command(); its effect is to modify all of the delays embedded in send_command (for example, a delay_factor=2 will double all of the delays; a delay_factor=3 will make delay by 3 times). In our case, file transfer requires ~262 seconds therefore delay_factor=3 would suffice. Since, network delay may cause further slowness hence to be on safer side we can consider delay_factor=4

try:
OUTPUT = OUTPUT + CONNECT.send_command('\n', expect_string=r'#', delay_factor=4)
except Exception:
EXECUTION = time.time() - start
print('Total time to execute this code is ' + str(EXECUTION))
CONNECT.disconnect()
raise
else:
EXECUTION = time.time() - start
print('Total time to execute this code is ' + str(EXECUTION))
print(OUTPUT)
CONNECT.disconnect()
$ python demo_pexpect.py
Total time to execute this code is 356.93184757232666
Address or name of remote host []? Destination filename [test.bin]? !!.!.!!.!.!!.!!.!.!.!!.!.!!.!.!.!.!!!.!.!!.!.!!.!.!.!
2431050 bytes copied in 345.926 secs (7028 bytes/sec)

This time program runs successfully and no error is observed.

Total time to execute this code is 8.49181580543518

Python package – “netmiko”

It’s a very important package used for Network Automation. This is quite similar to “paramiko” and used for SSH connection and executing cetain commands to a Variety of Network Devices.

Another advantage of “netmiko” is that it works with variety of vendors. Some of the common and mostly used supported platform types and their abbreviations to be called in Netmiko are as follows. Depending upon the selection of the platform type, Netmiko can understand the returned prompt and the correct way to SSH into the specific device. 

arista_eos: AristaSSH,
aruba_os: ArubaSSH,
avaya_ers: AvayaErsSSH,
avaya_vsp: AvayaVspSSH,
checkpoint_gaia: CheckPointGaiaSSH,
cisco_asa: CiscoAsaSSH,
cisco_ios: CiscoIosBase,
cisco_nxos: CiscoNxosSSH,
cisco_wlc: CiscoWlcSSH,
cisco_xe: CiscoIosBase,
cisco_xr: CiscoXrSSH,
f5_ltm: F5LtmSSH,
fortinet: FortinetSSH,
juniper: JuniperSSH,
juniper_junos: JuniperSSH,
linux: LinuxSSH,
mellanox_ssh: MellanoxSSH,
paloalto_panos: PaloAltoPanosSSH,

Here’s an example of a simple script to log in to the router and show the “show ip int brief” output:

from netmiko import ConnectHandler

# Define a connection Handler i.e. Device dictionary
CISCO_3850 = {
    'device_type': 'cisco_xe',
    'ip': '192.168.1.1',
    'username': 'mrcissp',
    'password': 'networkautomation',
    'port': '22',          # optional, defaults to 22
    'secret': '', # optional, defaults to ''
}
# Establish an SSH connection to the device by passing in the device dictionary.
CONNECT = ConnectHandler(**CISCO_3850)

# Execute show commands.
OUTPUT = CONNECT.send_command('show ip int brief')
print(OUTPUT)

# Cleanly disconnecting the connection 
CONNECT.disconnect()

Let’s take another example where we will make some changes to the running configuration to a network device i.e. in our case is router.

Refer to below program if we need to change/add the description of GigabitEthernet 0/0/0 router interface.

# Execute Configuration commands.
OUTPUT = CONNECT.send_command('show run int gi0/0/0')
print(OUTPUT)

CONFIG_COMMAND_SET = ['int gi0/0/0', 'description PRODUCTION']
OUTPUT = CONNECT.send_config_set(CONFIG_COMMAND_SET)
print(OUTPUT)

OUTPUT = CONNECT.send_command('show run int gi0/0/0')
print(OUTPUT)
$ python demo_netmiko.py
Building configuration...

Current configuration : 99 bytes
!
interface GigabitEthernet0/0/0
 description PROD
 no ip address
 shutdown
 negotiation auto
end

config term
Enter configuration commands, one per line.  End with CNTL/Z.
Router(config)#int GigabitEthernet0/0/0
Router(config-if)#description TEST
Router(config-if)#end
Router#
Building configuration...

Current configuration : 99 bytes
!
interface GigabitEthernet0/0/0
 description TEST
 no ip address
 shutdown
 negotiation auto
end

As we can see, for config push, we do not have to perform any additional configurations but just specify the commands in the same order as we send them manually to the router in a list, and pass that list as an argument to the send_config_set function. The output in Before config push is a simple output of the g0/0/0 interface, but the output under After config push now has the description that we configured using the list of commands.

Exception handling in “netmiko”

When we design our Python script, we assume that the device is up and running and also that the user has provided the correct credentials, which is not always the case. Sometimes there’s a network connectivity issue between Python and the remote device or the user enters the wrong credentials. Usually, python will throw an exception if this happens and will exit, which is not the optimum solution.

In this section, we will demonstrate how to handle the different exception with “netmiko”. The first one is “AuthenticationException”, and will catch the authentication errors in the remote device. The second class is “NetMikoTimeoutException”, which will catch timeouts or any connectivity. Refer to below program

from netmiko import ConnectHandler
from netmiko.ssh_exception import NetMikoTimeoutException
from netmiko.ssh_exception import NetMikoAuthenticationException

# Handling exception
try:
    CONNECT = ConnectHandler(**CISCO_3850)
except NetMikoTimeoutException:
    print('Failed to connect to ' + CISCO_3850['ip'] + ' due to connection issue' + '- Verify Network connectivity')
except NetMikoAuthenticationException:
    print('Failed to connect to ' + CISCO_3850['ip'] + ' due to Wrong Credential' + '- Verify them')
else:
    CONNECT.disconnect()

Let’s execute this for two cases

  1. A dummy IP
  2. Wrong Credentials
$python demo_netmiko.py
 Failed to connect to 10.24.7.4 due to connection issue- Verify Network connectivity
$python demo_netmiko.py
Failed to connect to 10.24.7.3 due to Wrong Credential- Verify them

There is another important method find_prompt() which is used to find the current prompt of the executed commands.

# Finding the current prompt i.e. enable prompt
CONNECT = ConnectHandler(**CISCO_3850)
PROMPT = CONNECT.find_prompt()
print('Our current prompt is ' + PROMPT)
$python demo_netmiko.py
Our current prompt is Router#

Python package – “getpass”

In Networking world, one of the most common thing user would like to do is to access devices over SSH connection. As we know SSH is an encrypted protocol and require users to prove authenticity which in turn requires a password.

Before I will start demonstrating – lets understand how to take input from user in Python. Refer to below code for reference

# Let us demonstrate this without "getpass"
# Python program showing a use of raw_input()
USERNAME = input("Enter your login name : ")
print(USERNAME)
$ python ssh_input.py
Enter your login name : MRCISSP
MRCISSP

If we observe the above output; entered username can be clearly seen on the screen. Many similar programs that interact with the user via the terminal need to ask the user for password values without showing what the user types on the screen. The “getpass” module provides a portable way to handle such password prompts securely. Let’s walk through some examples to understand its implementation.

# Lets understand the purpose of "getpass" package
# Primary Goals
#       1. Reading password without display
# Our objective is to create a Python program name "ssh_input" which accept these inputs
#       1. Login user : mrcissp
#       2. Login password : pythonfornetworkengineers
# & ensure that "Login password" must not be visible on console.

# Let us demonstrate this without "getpass"
# Python program showing a use of raw_input()
USERNAME = input("Enter your login name : ")
print('Username entered is ' + USERNAME)

# A simple Python program to demonstrate  getpass.getpass() to read password
import getpass

try:
    PASSWORD = getpass.getpass()
except Exception as error:
    print('ERROR', error)
else:
    print('Password entered is', PASSWORD)
$ python ssh_input.py
Enter your login name : mrcissp
Username entered is mrcissp
Password:
Password entered is pythonfornetworkengineers

As you can observe from the above output – password is actually hidden from the console.