Search by MAC address on Juniper switches
On the local network, you often need to know which switch port the device’s MAC address is on. The problem is solved easily if there are several switches in the network, but when there are more than 30 of them, everything becomes much more complicated. I want to share a small Python script that looks for the desired MAC address on the network and returns the name and port of the switch on which this MAC is registered.

Constructive criticism is welcome. Details under the cut.
If the network design is correct, that is, the root switch CORE , to which distribution switches DS (Distribution Switch) are connected , and, in turn, switches of access level AS (Access Switch). This rule is not always executed, access switches can be connected in series. In any case, the port of the upstream switch contains all the MAC addresses of the devices connected to the downstream switch.
For example, if the device of interest to us is connected to the AS3 switch , then, starting the search with CORE , we will find this address on the port leading to DS1 . By logging on to the DS1 , we will discover this MAC on the port leading toAS2 , going to AS2 , we will see that it leads us to AS3 , and only on AS3 will we find the specific port to which the device of interest is connected.
I didn’t want to do it all with my hands, sort through all the switches in the loop and determine where uplink was and where not too, so the next solution was born, which I want to share.
To find the MAC 08: 62: 66: c7: b3: 45 on the Juniper switch, run the following command:
If there is such a MAC, the answer will be as follows:
The last column will be the interface name of the switch on which the MAC is registered. But how to understand where this interface leads? And here Interface Descriptions come to the rescue . These are lines in the switch configuration file that allow you to assign text labels to interfaces.
Team
will show the following:
When configured, we indicate that this interface leads to the downstream switch:
The proposed script will do the following:
Thus, the script will go through all the network switches, starting with the kernel, and will try to find the necessary MAC. For successful work, it is enough to keep descriptions on the interfaces up to date, and the topology can be of almost any complexity.
An example of the script:
If the MAC is missing, we get
The last line is the switch and port of interest to us, but at the same time we can track the entire search path.
The full code is under the spoiler, thank you for your attention.

Constructive criticism is welcome. Details under the cut.
If the network design is correct, that is, the root switch CORE , to which distribution switches DS (Distribution Switch) are connected , and, in turn, switches of access level AS (Access Switch). This rule is not always executed, access switches can be connected in series. In any case, the port of the upstream switch contains all the MAC addresses of the devices connected to the downstream switch.
For example, if the device of interest to us is connected to the AS3 switch , then, starting the search with CORE , we will find this address on the port leading to DS1 . By logging on to the DS1 , we will discover this MAC on the port leading toAS2 , going to AS2 , we will see that it leads us to AS3 , and only on AS3 will we find the specific port to which the device of interest is connected.
I didn’t want to do it all with my hands, sort through all the switches in the loop and determine where uplink was and where not too, so the next solution was born, which I want to share.
A bit of theory.
To find the MAC 08: 62: 66: c7: b3: 45 on the Juniper switch, run the following command:
show ethernet-switching table | match08:62:66:c7:b3:45
If there is such a MAC, the answer will be as follows:
vlan151 08:62:66:c7:b3:45 D - xe-0/0/23.0
The last column will be the interface name of the switch on which the MAC is registered. But how to understand where this interface leads? And here Interface Descriptions come to the rescue . These are lines in the switch configuration file that allow you to assign text labels to interfaces.
Team
show interfaces xe-0/0/23 descriptions
will show the following:
Interface Admin Link Description
xe-0/0/23 up up SW>DS1
When configured, we indicate that this interface leads to the downstream switch:
set interfaces xe-0/0/23 description SW>DS1
Implementation
The proposed script will do the following:
- connect via SSH to the root switch;
- check which interface is the MAC address transmitted in the parameters;
- check the description of this interface;
- if the interface leads to the switch, recursively go to the next switch in the chain.
#в этот список будут складываться результаты поиска
searchpass = []
#main функция принимает в качестве параметра MAC-адрес и вызывает функцию checkswitch, в которую в качестве параметра передает имя корневого коммутатора и MAC-адрес. Весь результат поиска складывается в список searchpass в формате json.defmain(argv):
mac_addr = argv[0]
checkswitch('CORE',mac_addr)
for switch in searchpass:
print (json.dumps(switch, ensure_ascii=False))
if __name__ == "__main__":
main(sys.argv[1:])
#функция рекурсивного поиска MAC-адресаdefcheckswitch(hostname,mac_addr):try:
#создаем пустой словарь возвращаемых значений, ключу host присваиваем имя коммутатора
returnvalue = {}
returnvalue['host']=hostname
#sendCommand подключается к заданному коммутатору по SSH и вызывает команду поиска MAC-адреса в таблице коммутации
answer = sendCommand(hostname,'show ethernet-switching table | match '+mac_addr)
#делим строку на колонки, в последней находится имя интерфейса#vlan151 08:62:66:c7:b3:45 D - xe-0/0/23.0if(answer!=0):
iface = answer.split()[4]
returnvalue['iface']=iface
#проверяем description интерфейса, нужно отрезать последние 2 символа .0 от имени и забрать последнюю строку#xe-0/0/23 up up SW>DS01
answer = sendCommand(hostname,'show interfaces '+iface[:-2]+' descriptions | last 1 | no-more')
iface = answer.split()
#Если description на интерфейсе есть, записываем его в словарь и проверяем, начинается ли он с SW>. Если да, отрезаем эти 3 символа, берем имя следующего коммутатора и рекурсивно вызываем функцию снова. if(len(iface)>2):
iface=iface[3]
returnvalue['description']=iface
else:
returnvalue['description']='none'
searchpass.append(returnvalue)
if (iface[:3]=='SW>'):
checkswitch(iface[3:],mac_addr)
else:
returnvalue['iface']='none'
searchpass.append(returnvalue)
except Exception as e:
print(e)
Thus, the script will go through all the network switches, starting with the kernel, and will try to find the necessary MAC. For successful work, it is enough to keep descriptions on the interfaces up to date, and the topology can be of almost any complexity.
An example of the script:
python findmac.py 00:17:fc:21:e8:f9
{"host": "CORE", "iface": "xe-0/0/23.0", "description": "SW>DS1"}
{"host": "DS1", "iface": "xe-0/0/11.0", "description": "SW>AS2"}
{"host": "AS2", "iface": "xe-1/0/1.0", "description": "SW>AS3"}
{"host": "AS3", "iface": "ge-0/0/26.0", "description": "none"}
If the MAC is missing, we get
{"host": "CORE", "iface": "none"}
The last line is the switch and port of interest to us, but at the same time we can track the entire search path.
The full code is under the spoiler, thank you for your attention.
findmac.py
import paramiko
import time
import sys
import json
import threading
import logging
login = 'user1'
password = 'pass1234'
searchpass = []
port = 22classLogPipe(threading.Thread):def__init__(self, level):
threading.Thread.__init__(self)
self.daemon = False
self.level = level
self.fdRead, self.fdWrite = os.pipe()
self.pipeReader = os.fdopen(self.fdRead)
self.start()
deffileno(self):return self.fdWrite
defrun(self):for line in iter(self.pipeReader.readline, ''):
logging.log(self.level, line.strip('\n'))
self.pipeReader.close()
defclose(self):
os.close(self.fdWrite)
defexecute_ssh_command(host, port, username, password, command):try:
# Create the SSH client.
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
# Connect to the host.
ssh.connect(host, port, username, password, look_for_keys=False)
# Send the command (non-blocking)
stdin, stdout, stderr = ssh.exec_command(command)
# Wait for the command to terminatewhilenot stdout.channel.exit_status_ready() andnot stdout.channel.recv_ready():
time.sleep(1)
stdoutstring = stdout.readlines()
stderrstring = stderr.readlines()
return stdoutstring, stderrstring
finally:
if ssh isnotNone:
# Close client connection.
ssh.close()
defsendCommand(hostname,command):
returnvalue = 0
logging.info('Host '+hostname+', command: '+command)
Try:
#add .mydomain for FQDN
(stdoutstring, stderrstring) = execute_ssh_command(hostname+'.mydomain', port, login, password, command+'\n')
if (len(stdoutstring)>0):
logging.info(stdoutstring[0])
if (len(stderrstring)>0):
logging.info(stderrstring[0])
except Exception as e:
return returnvalue
else:
returnvalue = stdoutstring[0]
finally:
return returnvalue
defcheckswitch(hostname,mac_addr):try:
returnvalue = {}
returnvalue['host']=hostname
answer = sendCommand(hostname,'show ethernet-switching table | match '+mac_addr)
if(answer!=0):
iface = answer.split()[4]
returnvalue['iface']=iface
#cut .0 prefix in the interface name
answer = sendCommand(hostname,'show interfaces '+iface[:-2]+' descriptions | last 1 | no-more')
iface = answer.split()
if(len(iface)>2):
iface=iface[3]
returnvalue['description']=iface
else:
returnvalue['description']='none'
searchpass.append(returnvalue)
if (iface[:3]=='SW>'):
checkswitch(iface[3:],mac_addr)
else:
returnvalue['iface']='none'
searchpass.append(returnvalue)
except Exception as e:
logging.info(e)
defmain(argv):
mac_addr = argv[0]
#configure log
logging.basicConfig(filename='/var/log/findmac.log', level=logging.INFO, format='%(asctime)s %(message)s')
logging.info('Find MAC: '+mac_addr)
checkswitch('CORE',mac_addr)
for switch in searchpass:
print (json.dumps(switch, ensure_ascii=False))
if __name__ == "__main__":
main(sys.argv[1:])