My go to languages are Perl, PHP or good old bash. Everyone else seems to be using Python and up until now I had managed to steer clear of it. Mostly because I thought I didn’t need it but also because compared to languages that I’m familiar with, it looked odd. Just the structure of Python, having to use indentation, put me off.

But today I wrote my first Python script to help with an NX-OS upgrade, going from 6.2 to 7.3, via 7.2. I wanted to be able to quickly and easily collect the output from a number of different commands across 5 different VDCs at various times during the upgrade process, allowing me to diff the output to spot any potential issues. Usually I’d just hack something together, probably using a bash script, clogin (from RANCID) and redirecting output from stdout to a file.

There are a vast array of well maintained libraries for Python, such as Netmiko, that make interacting with network devices really easy. Because of this, development is quick. Even for someone who has never written anything in Python before.

My initial script was quick and dirty. It did the job but used hardcoded settings, such as the name of the config file to use. Then I decided to explore Python a bit more and added better error checking, command line options, output to a file rather than redirecting stdout etc.

It uses an INI file that allows me to execute a set of common commands on all devices, as defined under the “all” section and then any other section defines a device. Each section (apart from “all”) needs a “host” key and a “commands” key.

[all]
commands = sh clock,sh ver,sh port-channel summ,sh int status

[n7k-a-admin]
host = 10.10.100.39
commands = sh mod

[n7k-a-wan]
host = 10.10.100.33
commands = sh vpc

[n7k-a-core]
host = 10.10.100.7
commands = sh vpc,sh fex,sh fex 197

[n7k-a-transit]
host = 10.10.100.31
commands = sh ip eigrp neigh

[n7k-a-otv]
host = 10.10.100.29
commands = sh ip eigrp neigh,sh otv site,sh otv adj,sh otv vlan

And the script:

#!/usr/bin/env python3
#
# Copyright (c) 2021 David Ramsden
#
# This software is provided 'as-is', without any express or implied
# warranty. In no event will the authors be held liable for any damages
# arising from the use of this software.
#
# Permission is granted to anyone to use this software for any purpose,
# including commercial applications, and to alter it and redistribute it
# freely, subject to the following restrictions:
#
# 1. The origin of this software must not be misrepresented; you must not
#    claim that you wrote the original software. If you use this software
#    in a product, an acknowledgment in the product documentation would be
#    appreciated but is not required.
# 2. Altered source versions must be plainly marked as such, and must not be
#    misrepresented as being the original software.
# 3. This notice may not be removed or altered from any source distribution.
#

import sys, getopt, getpass
from netmiko import ConnectHandler
from configparser import ConfigParser

def usage():
	print(f'{__file__} -c <config ini> -o <output file> [-u <username>]')
	return

def command_header(switch, command, fh):
	print('*' * 80, file=fh)
	print(f'* {switch.upper()}: {command}', file=fh)
	print('*' * 80, file=fh)
	return

def main():
	try:
		opts, args = getopt.getopt(sys.argv[1:], 'c:o:u:', ['config=', 'output=', 'username='])
		if not len(opts):
			raise Exception('option missing')
	except Exception as err:
		usage()
		print(err)
		sys.exit(2)

	try:
		for opt, arg in opts:
			if opt in ('-c', '--config'):
				if arg is None:
					raise Exception('no config ini provided')
				config_file = arg
			elif opt in ('-o', '--output'):
				if arg is None:
					raise Exception('no output file provided')
				output_file = arg
			elif opt in ('-u', '--username'):
				if arg is None:
					raise Exception('no username provided')
				username = arg
			else:
				raise Exception('unknown option')
	except Exception as err:
		usage()
		print(err)
		sys.exit(2)

	# Instansiate ConfigParser.
	config = ConfigParser()

	# Get current user if the username option was not provided.
	try:
		username
	except NameError:
		username = getpass.getuser()

	# Get password to use.
	password = getpass.getpass(f'Password for {username}: ')

	# Read config file.
	try:
		with open(config_file) as f:
			config.read_file(f)
			f.close()
	except Exception as err:
		print(f'Unable to read config file: {err}')
		sys.exit(2)

	# Open output file for writing.
	try:
		outfile = open(output_file, 'w')
	except Exception as err:
		print(f'Unable to open {output_file} for writing: {err}')
		sys.exit(2)

	# Parse each section, where a section represents a switch.
	for switch in config.sections():
		# Ignore the 'all' section.
		if switch == 'all':
			continue

		# Set up device to connect to.
		device = {
			'device_type': 'cisco_nxos',
			'host': config.get(switch, 'host'),
			'username': username,
			'password': password,
			'fast_cli': True,
		}

		# Connect.
		print(f"Connecting to {switch.upper()} ({config.get(switch, 'host')}):")
		try:
			net_connect = ConnectHandler(**device)
		except Exception as err:
			print(f'Unable to connect: {err}')
			continue

		# Execute commands from the 'all' section.
		for command in config.get('all', 'commands').split(','):
			print(f'\tSend: {command}')
			command_header(switch, command, outfile)
			print(net_connect.send_command(command), file=outfile)

		# Execute commands specific to this switch.
		for command in config.get(switch, 'commands').split(','):
			print(f'\tSend: {command}')
			command_header(switch, command, outfile)
			print(net_connect.send_command(command), file=outfile)

		# Disconnect.
		print('Disconnecting.')
		net_connect.disconnect()

	outfile.close()

if __name__ == '__main__':
	main()

Of course in more recent version of NX-OS one can use the “snapshot” feature.

Leave a Reply

Your email address will not be published.

This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.