Skip to content

Commit 5c2e7c1

Browse files
committed
NXOS Account Parse Project
1 parent 0f4e7bf commit 5c2e7c1

File tree

3 files changed

+340
-0
lines changed

3 files changed

+340
-0
lines changed

projects/nxos_account_parse/README.md

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# NXOS Account Parse
2+
3+
## Basic Overview
4+
5+
### Description
6+
7+
Generate a spreadsheet of all changes on a Cisco Nexus device!
8+
9+
Cisco Nexus has a very useful command known as *"show accounting log all"*. This command keeps track of changes performed on the device. This includes what the changes were, as well as who did the changes.
10+
11+
The output, unfortunately, is...not ideal, to put it lightly. That is where automation comes into play! We'll take the messy output and *transform* it into something a lot more useful.
12+
13+
The idea of this project is to showcase an example where automation is used to *transform* data into something a lot more meaningful.
14+
15+
### Demonstration
16+
17+
![](https://github.com/syedur-rahman/networkcoder/blob/master/images/nxos_account_parse.gif)
18+
19+
### Requirements
20+
21+
This script was designed to be used with Python 3.
22+
23+
You must install the following libraries as well.
24+
25+
```bash
26+
colorama==0.4.3
27+
netmiko==2.4.2
28+
```
29+
30+
### Original Output
31+
32+
```
33+
Thu Jan 30 01:41:15 2020:type=update:id=192.168.12.138@pts/3:user=frank:cmd=configure terminal ; feature scp-server (SUCCESS)
34+
Mon Feb 10 09:30:45 2020:type=update:id=192.168.12.138@pts/3:user=ethan:cmd=configure terminal ; interface Ethernet1/10 ; description "this is a new interface" (SUCCESS)
35+
```
36+
37+
### Transformed Output
38+
39+
| DATE | USER | CHANGE |
40+
| ---------- | ----- | ------------------------------------------------------------ |
41+
| 2020-02-10 | ethan | interface Ethernet1/10<br />description "this is a new interface" |
42+
| 2020-01-30 | frank | feature scp-server |
43+
44+
## A Network Coder's Notes
45+
46+
*The below can be skipped by uninterested parties.*
47+
48+
### Logic Diagram
49+
50+
![](https://github.com/syedur-rahman/networkcoder/blob/master/images/nxos_account_parse.png)
51+
52+
### Parse Logic
53+
54+
![](https://github.com/syedur-rahman/networkcoder/blob/master/images/parse_accounting_log.png)
55+
56+
This type of parsing is a little bit more complex than regular parsing simply because we need to be able to transform the date into something a little more user friendly.
57+
58+
The way this is done here is via using a python library - `datetime`. This enables us to transform the Cisco date format into a date object, which we can later manipulate into whatever date format we prefer.
59+
60+
One strange aspect of the parse logic that you might have noticed is that we are *only* grabbing the last line of the configuration. This is used to avoid duplicates in our dataset. As you can see below, the output of the accounting log command has every line of the configuration as a separate entry.
61+
62+
```
63+
Mon Feb 10 09:30:45 2020:type=update:id=192.168.12.138@pts/3:user=ethan:cmd=configure terminal ; interface Ethernet1/10 (SUCCESS)
64+
Mon Feb 10 09:30:45 2020:type=update:id=192.168.12.138@pts/3:user=ethan:cmd=configure terminal ; interface Ethernet1/10 ; description "this is a new interface" (SUCCESS)
65+
```
66+
67+
Since the output contains redundant information, we should be fine with grabbing just the latest information from each line. This will remove the need to include duplication removal logic in our code.
68+
69+
### Conclusion
70+
71+
Parsing is the current unfortunate reality of automation as a Network Engineer. At the time of writing, the *'show accounting log all'* does not have a structured equivalent, so no matter how you decide to grab this data, it will need to be parsed to be of any use. Once you get used to parsing, automation should become a lot easier.
72+
73+
In future projects, we'll explore other kinds of parsing systems, such as **TextFSM**.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
DATE,USER,CHANGE
2+
2020-03-10,frank,"interface mgmt0
3+
no ip address
4+
interface mgmt0
5+
ip address 192.168.12.135 255.255.255.0
6+
ip name-server 192.168.12.1 use-vrf management"
7+
2020-02-01,ethan,"interface loopback100
8+
ip address 1.1.1.1 255.255.255.0
9+
interface loopback100
10+
ip address 2.2.2.2 255.255.255.0"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
""" nxos account parse
2+
generates a csv for the 'show accounting log' output
3+
specifically for the cisco nexus switches """
4+
5+
# import connection library
6+
import netmiko
7+
8+
# import cli coloring library
9+
import colorama
10+
11+
# import input library for passwords
12+
import getpass
13+
14+
# import the csv library
15+
import csv
16+
17+
# import datetime
18+
import datetime
19+
20+
# initialize colorama globally
21+
# required for windows and optional for other systems
22+
# additionally have colorama reset per print
23+
colorama.init(autoreset=True)
24+
25+
def _get_user_credentials():
26+
""" get user credentials
27+
this function initiates a prompt for the user's credentials
28+
29+
returns
30+
-------
31+
username
32+
str variable representing the username
33+
password
34+
str variable representing the password
35+
secret
36+
str variable representing the enable secret
37+
38+
"""
39+
40+
# iterate until the user provides username & password
41+
while True:
42+
# initialize username & password
43+
usr_msg = "Please provide your credentials."
44+
print(usr_msg)
45+
username = input("Username: ").strip()
46+
password = getpass.getpass("Password: ").strip()
47+
secret = getpass.getpass("Secret: ").strip()
48+
49+
# check if user entered username and password
50+
if username and password:
51+
pass
52+
else:
53+
# alert user that the username or password was not provided
54+
usr_msg = "\nCritical: Username or password was not provided."
55+
usr_msg += "Please try again.\n"
56+
print(colorama.Fore.RED + usr_msg)
57+
58+
# re-initates loop
59+
continue
60+
61+
# if secret was not provided by the user
62+
if not secret:
63+
# alert user that the secret was not provided
64+
usr_msg = "\nWarning: Please note that secret was not provided.\n"
65+
usr_msg += "Secret will be assumed to be the same as the password."
66+
print(colorama.Fore.CYAN + usr_msg)
67+
68+
# set secret and password sa the same
69+
secret = password
70+
71+
# return the username and password
72+
return username, password, secret
73+
74+
def _parse_show_accounting_log(device_output):
75+
""" parse show accounting log
76+
helper function with the parsing logic for the show accounting log command
77+
78+
returns
79+
-------
80+
parsed_data
81+
dict representing the parsed data of show accounting log
82+
will contain the date as key and user config data as values
83+
84+
example format listed below:
85+
86+
{ '2019-01-01': { 'admin': [ config_line_1, config_line_2] } }
87+
"""
88+
89+
# initialize parsed data
90+
parsed_data = {}
91+
92+
# iterate through the device output
93+
for line in device_output.splitlines():
94+
# skip if this was not a configuration update
95+
if 'configure' not in line:
96+
continue
97+
# skip if the configuration is not defined as success
98+
elif not line.strip().endswith('(SUCCESS)'):
99+
continue
100+
101+
# remove the success tag of the line
102+
line = line.replace('(SUCCESS)', '')
103+
104+
# cisco date format
105+
# this is the way cisco format's their date in the command
106+
cisco_date_format = '%a %b %d %H:%M:%S %Y:'
107+
108+
# date of change date format
109+
date_of_change_date_format = '%Y-%m-%d'
110+
111+
# determine date of change
112+
raw_date_from_line = line.split('type')[0]
113+
114+
# convert raw date from show command to a datetime object
115+
date_object = datetime.datetime.strptime(raw_date_from_line, cisco_date_format)
116+
117+
# convert datetime object to a string in the date of change date format
118+
date_of_change = date_object.strftime(date_of_change_date_format)
119+
120+
# grab user from the line
121+
user = line.split(':')[-2].split('=')[-1]
122+
123+
# grab change from the line
124+
change = line.split(';')[-1].strip()
125+
126+
# for formatting, add spaces to command based off ';' in line
127+
number_of_spaces = line.count(';') - 1
128+
change = number_of_spaces*' ' + change
129+
130+
# initialize parsed data with date of change
131+
if date_of_change not in parsed_data:
132+
parsed_data[date_of_change] = {}
133+
# initialize date of change with user
134+
if user not in parsed_data[date_of_change]:
135+
parsed_data[date_of_change][user] = []
136+
137+
# add change the user did that day to the parsed data
138+
parsed_data[date_of_change][user].append(change)
139+
140+
return parsed_data
141+
142+
def _save_parsed_data_to_csv(device, parsed_data):
143+
""" save parsed data to csv
144+
save the parsed data to csv which will be named after the device """
145+
146+
# initialize csv headers
147+
headers = ['DATE', 'USER', 'CHANGE']
148+
149+
# initialize filename
150+
filename = device + '.csv'
151+
152+
with open(filename, 'w') as csv_file:
153+
# initialize csv writer
154+
csv_writer = csv.writer(csv_file)
155+
156+
# write csv headers
157+
csv_writer.writerow(headers)
158+
159+
# iterate through the parsed data where latest date goes on top
160+
for date_of_change, user_changes in reversed(sorted(parsed_data.items())):
161+
# iterate through the user changes
162+
for user, changes in user_changes.items():
163+
# build a full change string
164+
full_change = ''
165+
for change in changes:
166+
full_change += change + '\n'
167+
full_change = full_change.strip()
168+
169+
# initialize row
170+
row = [date_of_change, user, full_change]
171+
172+
# write csv row
173+
csv_writer.writerow(row)
174+
175+
def nxos_account_parse():
176+
""" nxos account parse
177+
parses the accounting information on cisco nexus switches
178+
and generates a csv based off the parsed data for the user """
179+
180+
usr_msg = "# NXOS Account Parse Script\n"
181+
usr_msg += "# Generate a CSV of all changes on a Cisco Nexus device!\n"
182+
print(colorama.Fore.YELLOW + usr_msg)
183+
184+
# get credentials from the user
185+
try:
186+
username, password, secret = _get_user_credentials()
187+
188+
# exit the script upon user keyboard interrupt
189+
except KeyboardInterrupt:
190+
usr_msg = "\n\nExiting Parse Script.\n"
191+
print(colorama.Fore.MAGENTA + usr_msg)
192+
193+
return
194+
195+
# keep running till user decides to exit out
196+
while True:
197+
# ask user for a nexus switch dns or ip
198+
usr_msg = "\nPlease provide nexus switch (type 'q' to quit): "
199+
usr_inp = input(usr_msg).strip().lower()
200+
201+
# quit loop if conditions are met
202+
if usr_inp == 'q':
203+
break
204+
205+
# assume user's input is device
206+
device = usr_inp
207+
208+
# build netmiko device profile
209+
network_device_profile = {
210+
'device_type': 'cisco_nxos',
211+
'ip': device,
212+
'username': username,
213+
'password': password,
214+
'secret': secret,
215+
}
216+
217+
# initialize the connection handler of netmiko
218+
usr_msg = "\nConnecting to " + device.lower()
219+
print(colorama.Fore.CYAN + usr_msg)
220+
try:
221+
net_connect = netmiko.ConnectHandler(**network_device_profile)
222+
223+
# in case of authentication failure
224+
# user will be informed and the program will exit
225+
except netmiko.ssh_exception.NetMikoAuthenticationException:
226+
usr_msg = "\nAuthentication Failure - Exiting Quick Deploy.\n"
227+
print(colorama.Fore.RED + usr_msg)
228+
229+
# exit out of loop
230+
break
231+
232+
# initialize show account log command
233+
command = 'show accounting log all'
234+
235+
# capture device output from show command
236+
usr_msg = "\nRunning 'show accounting log all'"
237+
print(colorama.Fore.CYAN + usr_msg)
238+
device_output = net_connect.send_command(command)
239+
240+
# disconnect from the nexus switch
241+
net_connect.disconnect()
242+
243+
# parse the device output
244+
usr_msg = "\nParsing 'show accounting log all'"
245+
print(colorama.Fore.CYAN + usr_msg)
246+
parsed_data = _parse_show_accounting_log(device_output)
247+
248+
# write parsed data to csv
249+
usr_msg = f"\nSaving parsed data to '{device.lower()}.csv'"
250+
print(colorama.Fore.CYAN + usr_msg)
251+
_save_parsed_data_to_csv(device, parsed_data)
252+
253+
usr_msg = "\nExiting Parse Script.\n"
254+
print(colorama.Fore.MAGENTA + usr_msg)
255+
256+
if __name__ == '__main__':
257+
nxos_account_parse()

0 commit comments

Comments
 (0)