Welcome back to this series on Diff`ing the network. In part 1 we looked at difflib
for performing network differentials when dealing with text-based, i.e unstructured data. If you missed it or need to recap see below.
We will now look at how to perform differential comparisons with Python dictionaries using jsondiff
.
jsondiff
jsondiff
is a tool that allows you to:
Diff JSON and JSON-like structures in Python.
In other words, we can compare 2 outputs, be it JSON or a Python dictionary. The differences are then returned as a Python dictionary, which we can then work with in a programmatic way to perform further actions.
Let's look at an example...
First, we install jsondiff
. As always we also install rich.print
to brighten things up a little! Learn more here.
$ pip3 install jsondiff rich
Next we collect the ARP cache using PyATS's Genie Parse (course here).
from genie import testbed
testbed = testbed.load("./testbeds/002_internal_testbed.yml")
device = testbed.devices["spine1-nxos"]
device.connect(log_stdout=False)
arp1 = device.parse("show ip arp")
arp2 = device.parse("show ip arp")
The 2 outputs are then compared, like so:
from jsondiff import diff
arp_diff = diff(arp1, arp2)
If we look at the results we can see the differences (shown below). The differences include the age
for each of the neighbour entries. But we also see delete: ['Ethernet1/1']
.
from rich import print as rprint
rprint(arp_diff)
===
{
'interfaces': {
'Ethernet1/2': {'ipv4': {'neighbors': {'10.1.2.2': {'age': '00:17:47'}}}},
'Ethernet1/3': {'ipv4': {'neighbors': {'10.1.3.2': {'age': '00:04:05'}}}},
'Ethernet1/4': {'ipv4': {'neighbors': {'10.1.4.2': {'age': '00:03:18'}}}},
'Ethernet1/5': {'ipv4': {'neighbors': {'10.1.5.2': {'age': '00:10:04'}}}},
'Ethernet1/6': {'ipv4': {'neighbors': {'10.1.6.2': {'age': '00:03:09'}}}},
delete: ['Ethernet1/1']
},
'statistics': {'entries_total': 5}
}
This is all well and good, but what are the ARP details for the entries that are no longer present in arp2
. To see this information we use syntax=symmetric
, which includes the data that has changed from the starting data point e.g arp1
. For example:
arp_diff_symmetric = diff(arp1, arp2, syntax="symmetric")
rprint(arp_diff_symmetric)
===
{
'interfaces': {
'Ethernet1/2': {'ipv4': {'neighbors': {'10.1.2.2': {'age': ['00:16:15', '00:17:47']}}}},
'Ethernet1/3': {'ipv4': {'neighbors': {'10.1.3.2': {'age': ['00:02:33', '00:04:05']}}}},
'Ethernet1/4': {'ipv4': {'neighbors': {'10.1.4.2': {'age': ['00:01:47', '00:03:18']}}}},
'Ethernet1/5': {'ipv4': {'neighbors': {'10.1.5.2': {'age': ['00:08:33', '00:10:04']}}}},
'Ethernet1/6': {'ipv4': {'neighbors': {'10.1.6.2': {'age': ['00:01:37', '00:03:09']}}}},
delete: {
'Ethernet1/1': {
'ipv4': {
'neighbors': {
'10.1.1.2': {
'ip': '10.1.1.2',
'link_layer_address': '5000.0009.0000',
'physical_interface': 'Ethernet1/1',
'origin': 'dynamic',
'age': '00:00:09'
}
}
}
}
}
},
'statistics': {'entries_total': [6, 5]}
}
To access the missing ARP entry we perform a lookup against delete
using jd.delete
(shown below).
import jsondiff as jd
deleted_arps_symmetric = arp_diff['interfaces'].get(jd.delete)
rprint(deleted_arps_symmetric)
===
{
'Ethernet1/1': {
'ipv4': {
'neighbors': {
'10.1.1.2': {'ip': '10.1.1.2', 'link_layer_address': '5000.0009.0000', 'physical_interface': 'Ethernet1/1', 'origin': 'dynamic', 'age': '00:00:09'}
}
}
}
}
Now we have the details of our missing ARP entry as a Python dictionary, we can use it to perform further actions. Such as use the IP to collect the device details from Netbox (via pynetbox
).
Other examples could include extracting certain keys and values to build an HTML report, trigger a Slack message to notify different teams within the business, or create troubleshooting scripts that support teams can run to quickly pinpoint issues.