[python]Multiprocessing with Netmiko

Network diagram
network1

What I want to achieve
There are three vIOS routers which I need to configure with some portion of harden configuration derived from Cisco’s hardening guide.

The configuration portion I chose is static, hence no dynamic information like the post I did last time. The purpose is to record how multiprocessing can be done together with netmiko.

The configuration required are:

  1. Set up a banner motd
  2. Set the access-list to only allow 192.168.1.0/24 to ssh to the routers
  3. Set the ssh idle timeout to 5mins only.
  4. Enable service password-encryption
  5. Enable tcp keep alive
  6. Enable buffer overflow detection
  7. Enforce ssh credential must be typed in within 1 min
  8. Maximum 3 times retries of ssh login
  9. Disable aux0

How the script works

  1. Save show ip int brief output to show_ip_int_brief.txt
  2. get_ipaddress.py will get the mgmt ip addresses
  3. Check if p.txt which is the password file and enc.key which is the encryption key are present
  4. if both enc.key and p.txt are not found, the script prompts admin to enter the password. The script is written with assumption that all routers have the same username and password.
  5. Decrypts the p.txt and get the plaintext password
  6. The ip addresses and passwords are information for the device dictionary required for netmiko
  7. Assign each process to a device
  8. Start the processes to send the commands over to routers with netmiko.

The How-To presented in the configuration

  1. Demonstrate at minimum what is required to use netmiko to send configuration to cisco routers
  2. How to encrypt and decrypt a file
  3. How to attach each process to a device and send the configuration in parallel.
  4. How to validate ip address, the solution is not to use regex, regex cannot count the bytes hence the best method is to use builtin ipaddress module to accurate validate ip address
  5. How to use regular expression to get the string you want

Script structure
venv1

get_ipaddress.py
This script gets the ip addresses from the show ip int brief output.

import re
from ipaddress import IPv4Address

# the purpose to find numbers separate by dots not to match ip address.
pattern = r'\S*\d+\.\d+\.\d+\.\d+\S*'
def get_mgmt_addresses():
    # read from the show ip int brief outputs that are saved within show_ip_int_brief.txt
    with open('routers\show_ip_int_brief.txt', 'r') as file:
        data = file.read()
    gather_ip = []
    regex = re.compile(pattern)
    # for each line search for the pattern.
    for line in data.splitlines():
        # if number with dots is found
        if regex.findall(line):
            try:
                # IPv4Address is used to evaluate if it is indeed ip
                # check against IPv4Address, using IPv4Address returns IPv4Address object.
                # hence need to type cast string to IPv4Address object.
                gather_ip.append(str(IPv4Address(regex.findall(line)[0])))
            except:
                # do not care if IPv4Address returns an exception.
                # if IPv4Address does not throw exception means the ip address is valid.
                pass
    # return the gather_ip list
    return gather_ip

password_protect.py
This script encrypts and decrypts the password file – p.txt.

from getpass import getpass
from cryptography.fernet import Fernet

key_file_path = "vault\enc.key"
p_file_path = "vault\p.txt"

# use for generate a symmetric key.
# then store the key as a file.
def generate_key_file():
    key = Fernet.generate_key()
    with open(key_file_path, 'wb') as file:
        file.write(key)

# Use the key for encryption and decryption.
def use_key():
    with open(key_file_path, 'rb') as file:
        return file.read()

# Get the password from user.
# the password is a string, before passing to Fernet for encryption
# the plaintext has to be converted to bytes, which is why encode('utf-8').
def store_mgmt_password():
    password = getpass('Enter your password, as password is not found: ')
    key = use_key()
    fernet = Fernet(key)
    # convert the plaintext password into bytes
    # and store the encrypted byte to enc_password.
    enc_password = fernet.encrypt(password.encode('utf-8'))
    # save the encrypted password to p.txt.
    with open(p_file_path, 'wb') as file:
        file.write(enc_password)

# Decrypt the p.txt and get the plaintext password.
def get_mgmt_password():
    key = use_key()
    fernet = Fernet(key)
    with open(p_file_path, 'rb') as file:
        password_in_bytes = file.read()
    # The content in the p.txt is byte, which is why decode('utf-8') to convert to string.
    return fernet.decrypt(password_in_bytes).decode('utf-8')

devices.py
This script gathers the list of dictionaries of device, which is information required by netmiko to send configuration to cisco routers.

from get_ipaddress import get_mgmt_addresses
from password_protect import (
p_file_path, key_file_path, store_mgmt_password, get_mgmt_password, generate_key_file
)
from os.path import exists

device_list = []
ip_addresses = get_mgmt_addresses()

# check for symmetric key.
# if does not exists create one.
if not exists(key_file_path):
    generate_key_file()

# check if p.txt is present or not, if not prompt for one.
# then encrypt the password and save as p.txt.
if not exists(p_file_path):
    store_mgmt_password()

# required for passing dictionary over to netmiko.
# returns a list of dictionaries.
def devices():
    for ip_address in ip_addresses:
        device = {
            "ip": ip_address,
            "username": "cyruslab",
            "password": get_mgmt_password(),
            "device_type": "cisco_ios"
        }
        device_list.append(device)
    return device_list

main.py
This is the main script that gels all above scripts together.

from netmiko import ConnectHandler
from multiprocessing import Process
from devices import devices
from time import time

# this is a process container use to map a process to a device.
processes = []

# this fires the commands from the config.txt to all routers.
def send_cmd(device):
    with ConnectHandler(**device) as conn:
        conn.send_config_from_file("templates\config.txt")

if __name__ == '__main__':
    # for timing how long it takes to finish.
    start_time = time()
    # for each device attach to a process to send commands.
    for device in devices():
        p = Process(target=send_cmd, args=(device,))
        processes.append(p)

    # Start the process
    for process in processes:
        process.start()

    # wait for process to end before termination
    for process in processes:
        process.join()

    print("Script takes {} seconds to complete".format(time() - start_time))

Demonstration
This is the original state of the routers.
orig1

For the purpose of demonstration I have deleted the enc.key which is a symmetric key, and the p.txt which stores the password.

So I ran the main.py script which took 10 seconds, on my first attempt the script took 9 seconds.
ex1

The below are console logging which indicated that the user – cyruslab made configuration changes.
r1r2r3

The below are the changes pushed by the script.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s