Recently, I came across a task where I had to track down the switch ports the devices are connected to using the ARP table. If I want to search for just a bunch of devices then it is not an issue, I can just log in to the switches and find the ports. However, imagine if you have to go through multiple VLANS and find hundreds of switch ports.
This was a manual, time-consuming task so, I decided to look into a way to automate it. Initially, I thought of using Ansible but couldn't really find a way to do it (please let me know in the comments if Ansible can be used)
After a bit of research, Napalm library appeared to be the ideal candidate for this task and I managed to write the script and decided to document the process to benefit others who are looking for a similar solution.
Assumptions
This blog post assumes you are somewhat familiar with the following Python topics
- Python import modules
- Python list and dictionary
- if statements
- for loops
Diagram
The example is based on the following diagram where four devices are connected between the two access switches.
When you run the script, this is the output you will see. The script also outputs a CSV file as shown below.
Why Napalm?
Napalm is a Python library that makes interacting with multi-vendor devices feel like a breeze. Initially, I thought of using Netmiko with TextFSM but then decided to go with Nalapm because it already outputs structured data.
Mapping Mac Address to Switch Port
A few things to note here:
vlan == 20
- I'm only interested in the devices connected to VLAN 20. You can remove this condition if you want to see all the devices on all the VLANs. You can also add an 'or' statement if you want to see the output for a few specific VLANs.napalm_output_switch[n]["interface"] != Gi0/1
- As both switches are connected via the distribution switch, the Mac addresses of the devices can also be seen on the other switch. By using this condition, I'm excluding theGi0/1
uplink interfaces.- getpass prompts the user for a value, password in our case, without echoing what the user types to the terminal. getpass then reads input from the user and saves it as a string.
- The switches are added to the Python List called 'switches'. In this example, I'm only using two switches.
from napalm import get_network_driver
import json
import csv
import getpass
passwd = getpass.getpass('Please enter the password: ') # Reads the output from the user and save it as a string
driver = get_network_driver('ios')
cisco_01 = {
"hostname": 'cisco-router-01.packet.lan',
"username": "cisco",
"password": passwd,
"optional_args": {"secret": passwd}
}
switches = ['switch-01.packet.lan', 'switch-02.packet.lan']
switch_list = list()
for switch_ip in switches:
switch_dict = {
"hostname": switch_ip,
"username": "cisco",
"password": passwd,
"optional_args": {"secret": passwd}
}
switch_list.append(switch_dict)
device_router = driver(**cisco_01)
device_router.open()
print(f'Connecting to {cisco_01["hostname"]}')
napalm_output_router = device_router.get_arp_table()
print(json.dumps(napalm_output_router, indent=4))
device_router.close()
for each_switch in switch_list:
device_switch = driver(**each_switch)
device_switch.open()
print(f'Connecting to {each_switch["hostname"]}')
napalm_output_switch = device_switch.get_mac_address_table()
print(json.dumps(napalm_output_switch, indent=4))
for i in range(len(napalm_output_router)):
arp_mac = napalm_output_router[i]["mac"]
for n in range(len(napalm_output_switch)):
if arp_mac in napalm_output_switch[n]["mac"] and napalm_output_switch[n]["vlan"] == 20 and napalm_output_switch[n]["interface"] != 'Gi0/1':
print(f'{arp_mac} is connected to {each_switch["hostname"]} on {napalm_output_switch[n]["interface"]} and the IP is {napalm_output_router[i]["ip"]} ')
with open('mac_data.csv', 'a', newline='') as csvfile:
writer = csv.writer(csvfile)
csvdata = (napalm_output_router[i]["ip"], arp_mac, each_switch["hostname"], napalm_output_switch[n]["interface"])
writer.writerow(csvdata)
device_switch.close()
Here is the breakdown of the code.
- Imports the required libraries - napalm, json, csv, and getpass.
- Prompts the user to enter a password, which is saved as a string variable called
passwd
using the getpass library. - Defines a dictionary
cisco_01
that contains the login details for a Cisco router with the hostnamecisco-router-01.packet.lan
. This dictionary also includes anoptional_args
field, which sets thesecret
to the value ofpasswd
. - Defines a list
switches
containing the hostnames of two switches, and then creates a list of dictionariesswitch_list
, where each dictionary contains the login details for one of the switches inswitches
list. - Uses
get_network_driver
from the napalm library to retrieve the driver for IOS devices and uses it to create adevice_router
object with the login details from cisco_01. - Opens a connection to the router using
device_router.open()
, prints a message indicating that it is connecting to the router, and retrieves the ARP table usingdevice_router.get_arp_table()
. The output is then printed in JSON format usingjson.dumps()
with an indentation of 4. - Closes the connection to the router using
device_router.close()
. - Loops over each dictionary in
switch_list
, connects to the switch using the driver object and the login details, retrieves the MAC address table usingdevice_switch.get_mac_address_table()
, and prints the output in JSON format. - Loops over each entry in the ARP table from the router, and for each entry, loops over each entry in the MAC address table from the switch.
- If the MAC address from the ARP table matches the MAC address from the MAC address table, and the VLAN is 20 and the interface is not Gi0/1, prints a message indicating that the MAC address is connected to the switch on the specified interface and IP address. It also writes the data to a CSV file called
mac_data.csv
. - Closes the connection to the switch using
device_switch.close()
.
The ARP table from the router and the Mac address tables from switches would be like the below. As you can see, the output is already structured as a Python dictionary. All we need to do is, pick each mac
from the ARP table and search it through the mac-address table using for loop
statements.
For example, let's pick the 4th item in the list which is 00:50:79:66:68:04
, if the mac address is found in the switches, Python will print the port number, switch name and the IP address.
[
{
"interface": "GigabitEthernet0/0",
"mac": "B4:2E:99:FC:83:76",
"ip": "10.10.0.10",
"age": 15.0
},
{
"interface": "GigabitEthernet0/0",
"mac": "50:00:00:01:00:00",
"ip": "10.10.20.17",
"age": -1.0
},
{
"interface": "GigabitEthernet0/1",
"mac": "50:00:00:01:00:01",
"ip": "192.168.15.1",
"age": -1.0
},
{
"interface": "GigabitEthernet0/1",
"mac": "00:50:79:66:68:04",
"ip": "192.168.15.11",
"age": 0.0
},
{
"interface": "GigabitEthernet0/1",
"mac": "00:50:79:66:68:06",
"ip": "192.168.15.12",
"age": 0.0
},
{
"interface": "GigabitEthernet0/1",
"mac": "00:50:79:66:68:07",
"ip": "192.168.15.13",
"age": 0.0
},
{
"interface": "GigabitEthernet0/1",
"mac": "00:50:79:66:68:08",
"ip": "192.168.15.14",
"age": 0.0
}
]
[
{
"mac": "50:00:00:01:00:00",
"interface": "Gi0/0",
"vlan": 10,
"static": false,
"active": true,
"moves": -1,
"last_move": -1.0
},
{
"mac": "50:00:00:05:00:00",
"interface": "Gi0/0",
"vlan": 10,
"static": false,
"active": true,
"moves": -1,
"last_move": -1.0
},
{
"mac": "B4:2E:99:FC:83:76",
"interface": "Gi0/0",
"vlan": 10,
"static": false,
"active": true,
"moves": -1,
"last_move": -1.0
},
{
"mac": "DC:A6:32:04:D2:56",
"interface": "Gi0/0",
"vlan": 10,
"static": false,
"active": true,
"moves": -1,
"last_move": -1.0
},
{
"mac": "00:50:79:66:68:04",
"interface": "Gi1/1",
"vlan": 20,
"static": false,
"active": true,
"moves": -1,
"last_move": -1.0
},
{
"mac": "00:50:79:66:68:07",
"interface": "Gi1/2",
"vlan": 20,
"static": false,
"active": true,
"moves": -1,
"last_move": -1.0
},
{
"mac": "50:00:00:01:00:01",
"interface": "Gi0/1",
"vlan": 20,
"static": false,
"active": true,
"moves": -1,
"last_move": -1.0
}
]
That's all for now, please let me know in the comments if there is a better way of doing this.