When working with pyATS, one of the first things that threw me off when working with the data that came back from my lab router was that the data was in a structure that I wasn't used to seeing. I really, really struggled with this, so I hope if you have found your way here, this blog helps you! The issue with the structure is that it comes back from a router as a dictionary.
This is the output from a vty connection to an 1841 I have running in my lab:
The information is taken from the running configuration of the router and printed on the screen in a human readable format. The same output through pyATS looks something like this:
For human readable this output loses points, however this output can be very powerful if you can iterate through the dictionary items and find the values you need from the dictionary keys. One of the first things to do is get a feel for the actual structure of this dictionary. The dictionary itself makes more sense when structured like this:{
'interface':{
'FastEthernet0/0':{
'ip_address': '203.0.113.1',
'interface_is_ok': 'YES',
'method': 'NVRAM',
'status': 'up',
'protocol': 'down'},
'FastEthernet0/1': {
'ip_address': '192.168.0.69',
'interface_is_ok': 'YES',
'method': 'NVRAM',
'status': 'up',
'protocol': 'up'},
'Serial0/0/0': {
'ip_address': 'unassigned',
'interface_is_ok': 'YES',
'method': 'NVRAM',
'status': 'administratively down',
'protocol': 'down'},
'Loopback0': {
'ip_address': '192.0.2.1',
'interface_is_ok': 'YES',
'method': 'NVRAM',
'status': 'up',
'protocol': 'up'}
}
}
To save you the pain and anguish of doing this every time you parse config and runtime information from your appliances, there is a nifty reference library on the DevNetCloud. The link to the site is here:
The trick to this is to remember that each comma represents a new item (which I have shown on a new line) and each open curly brace ({) denotes a new indent, whereas a closed curly brace (}) denotes going back an indent. Now all of a sudden the information makes much more sense. But how do you get the information you want from the dictionary?
As I've already mentioned, dictionaries are made up of items, which are made up of keys and values. The structure of each is shown below:
The next step to gaining our detail is to work through the dictionary line by line to gain the detail we want. The best way to do this is to use the Python Debugger (PDB). set the PDB to break your script on the line where you are parsing information from your router. In my case this line is on line 21 and states:
preoutput = device.parse("show ip interface brief")
$ python3 -m pdb YourScript.py
(Pdb) b 21
(Pdb) c
Do this by running PDB first, pressing 'b 21' (for break on 21), hit return followed by 'c' (for continue). Now you can start picking into the dictionary. If you don't know which line to break at you can press 'll', which will show you your script and the corresponding line numbers.
As you will see in the above (cleaned) output from the parse, the first entry in the dictionary is 'interface', which is then nested into the interfaces themselves. This structure only shows you the level you are working with when you ask the PDB to show you the keys, while showing the values will show you all values beneath that key:
(Pdb) preoutput.keys()
dict_keys(['interface'])
(Pdb) preoutput.values()
dict_values([{'FastEthernet0/0': {'ip_address': '203.0.113.1', 'interface_is_ok': 'YES', 'method': 'NVRAM', 'status': 'up', 'protocol': 'down'}, 'FastEthernet0/1': {'ip_address': '192.168.0.69', 'interface_is_ok': 'YES', 'method': 'NVRAM', 'status': 'up', 'protocol': 'up'}, 'Serial0/0/0': {'ip_address': 'unassigned', 'interface_is_ok': 'YES', 'method': 'NVRAM', 'status': 'administratively down', 'protocol': 'down'}, 'Loopback0': {'ip_address': '192.0.2.1', 'interface_is_ok': 'YES', 'method': 'NVRAM', 'status': 'up', 'protocol': 'up'}}])
Now if you start picking into the structure by adding the additional piece shown below in bold and red, you start to see how we start to work down into the structure of the code:
(Pdb) preoutput['interface'].keys()
dict_keys(['FastEthernet0/0', 'FastEthernet0/1', 'Serial0/0/0', 'Loopback0'])
There are still a mass of keys, although it's getting a little more specific:
(Pdb) preoutput['interface'].values()
dict_values([{'ip_address': '203.0.113.1', 'interface_is_ok': 'YES', 'method': 'NVRAM', 'status': 'up', 'protocol': 'down'}, {'ip_address': '192.168.0.69', 'interface_is_ok': 'YES', 'method': 'NVRAM', 'status': 'up', 'protocol': 'up'}, {'ip_address': 'unassigned', 'interface_is_ok': 'YES', 'method': 'NVRAM', 'status': 'administratively down', 'protocol': 'down'}, {'ip_address': '192.0.2.1', 'interface_is_ok': 'YES', 'method': 'NVRAM', 'status': 'up', 'protocol': 'up'}])
If we pick on FastEthernet0/0 and add another key to the drill-down, we see that the keys are as specific as they're going to get (again, in red and bold to show the changes in the command.
(Pdb) preoutput['interface']['FastEthernet0/0'].keys()
dict_keys(['ip_address', 'interface_is_ok', 'method', 'status', 'protocol'])
Now we're there! We can use the remaining command to extract the specific value we require, for this we'll use IP address as a value we want:
(Pdb) preoutput['interface']['FastEthernet0/0'].get('ip_address')
'203.0.113.1'
The command my not be clear to some so I'll try and illustrate what we have done:
You can now add this to any kind of variable you want as a line in your script. As you develop you can create loops that will run through the parse and provide the output of each interface of the device. This example is a little more specific but will get you started on the right track.
(Pdb) ipAddressTest = preoutput['interface']['FastEthernet0/0'].get('ip_address')
(Pdb) ipAddressTest
'203.0.113.1'
In my next blog I'll take you through exactly how to do this with a for loop and how to include emojis in your script to make things a little more fun.
Happy coding!
SC
Super helpful Simon! Thanks for writing this blog.
ReplyDeleteThanks for reading it! Happy coding.
DeleteAssuming pre-output is a dict, Use the following to 'prettify' your JSON for reading the structs
ReplyDeleteprint(json.dumps(preoutput, indent=4, sort_keys=True))
You can also use pprint, but i tend to avoid including extra libraries if i can
DeleteBeing as efficient as possible is always the name of the game Keef. Great recommendation! I generally don't do this if I'm picking out specific k, v pairs from a scripted output from pyATS because it's just more code, when the script doesn't need to see it.
DeleteHowever, human-readable this is a great recommendation. Thanks mate. Hope you're keeping well.