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.

    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:

    1. connect via SSH to the root switch;
    2. check which interface is the MAC address transmitted in the parameters;
    3. check the description of this interface;
    4. 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:])


    Also popular now: