Конструктивная критика приветствуется. Подробности под катом.
Если дизайн сети выполнен правильно, то есть корневой коммутатор CORE, к которому подключены коммутаторы распределения DS (Distribution Switch), а к ним, в свою очередь, коммутаторы уровня доступа AS (Access Switch). Это правило не всегда выполняется, коммутаторы доступа могут быть подключены последовательно. В любом случае, на порту вышестоящего коммутатора находятся все MAC-адреса устройств, подключенных к нижестоящему коммутатору.
Например, если интересующее нас устройство подключено к коммутатору AS3, то, начав поиск с CORE, мы найдем этот адрес на порту, ведущему к DS1. Зайдя на DS1, мы обнаружим этот MAC на порту, ведущему к AS2, зайдя на AS2, мы увидим, что он ведет нас к AS3, и только на AS3 мы найдем конкретный порт, к которому подключено интересующее нас устройство.
Делать это всё руками не хотелось, перебирать все коммутаторы в цикле и определять, где аплинк, а где нет тоже, поэтому родилось следующее решение, которым и хочу поделиться.
Немного теории.
Чтобы найти MAC 08:62:66:c7:b3:45 на коммутаторе Juniper, нужно выполнить следующую команду:
show ethernet-switching table | match 08:62:66:c7:b3:45
Если такой MAC есть, ответ будет следующим:
vlan151 08:62:66:c7:b3:45 D - xe-0/0/23.0
В последней колонке будет имя интерфейса коммутатора, на котором зарегистрирован MAC. Но как понять, куда ведет этот интерфейс? И тут на помощь приходят Interface Descriptions. Это строки в конфигурационном файле коммутатора, которые позволяют назначить текстовые метки интерфейсам.
Команда
show interfaces xe-0/0/23 descriptions
покажет следующее:
Interface Admin Link Description
xe-0/0/23 up up SW>DS1
При конфигурации мы указываем, что этот интерфейс ведет к нижестоящему коммутатору:
set interfaces xe-0/0/23 description SW>DS1
Реализация
Предлагаемый скрипт будет делать следующее:
- подключаться по SSH на корневой коммутатор;
- проверять, на каком интерфейсе находится передаваемый в параметрах MAC-адрес;
- проверять Description этого интерфейса;
- если интерфейс ведет к коммутатору, рекурсивно заходить на следующий по цепочке коммутатор.
#в этот список будут складываться результаты поиска
searchpass = []
#main функция принимает в качестве параметра MAC-адрес и вызывает функцию checkswitch, в которую в качестве параметра передает имя корневого коммутатора и MAC-адрес. Весь результат поиска складывается в список searchpass в формате json.
def main(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-адреса
def checkswitch(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.0
if(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)
Таким образом, скрипт пройдет по всем коммутаторам сети, начиная с ядра, и попытается найти нужный MAC. Для успешной работы достаточно поддерживать в актуальном состоянии descriptions на интерфейсах, а топология может быть практически любой сложности.
Пример работы скрипта:
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"}
Если MAC отсутствует, получим
{"host": "CORE", "iface": "none"}
Последняя строка – интересующий нас коммутатор и порт, но при этом мы можем отследить весь путь поиска.
Полный код – под спойлером, благодарю за внимание.
import paramiko
import time
import sys
import json
import threading
import logging
login = 'user1'
password = 'pass1234'
searchpass = []
port = 22
class LogPipe(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()
def fileno(self):
return self.fdWrite
def run(self):
for line in iter(self.pipeReader.readline, ''):
logging.log(self.level, line.strip('\n'))
self.pipeReader.close()
def close(self):
os.close(self.fdWrite)
def execute_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 terminate
while not stdout.channel.exit_status_ready() and not stdout.channel.recv_ready():
time.sleep(1)
stdoutstring = stdout.readlines()
stderrstring = stderr.readlines()
return stdoutstring, stderrstring
finally:
if ssh is not None:
# Close client connection.
ssh.close()
def sendCommand (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
def checkswitch(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)
def main(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:])
Комментарии (13)
Zagrebelion
13.08.2018 11:51show lldp neighbors interface .... | display xml
— чтобы не завязываться на формат дескрипшенов.alk0v Автор
13.08.2018 11:58Да, на счет lldp я думал, но с последнего свича в цепочке все равно хотелось получить дескрипшен и есть дополнительная проверка, что это именно downlink, по признаку «SW>».
Если этой проверки нет, скрипт может пытаться лезть по ssh на все подряд и тупить. С дескрипшенами процесс более контролируемый, хоть и менее универсальный.
Naves
13.08.2018 12:48cacti + mactrack plugin делает то же самое через snmp
PS тоже сначала чесались руки написать свой велосипед для SSH, но разум сказал: ищи готовое решение.
maxx_s
14.08.2018 16:29SNMP для этих целей гораздо практичней. Привезут вам цисок, будете переписывать?
И ещё — если устройство давно не рассылало широковещательных кадров его MAC вы ни на Core ни на DS вы не найдёте.alk0v Автор
14.08.2018 16:32У меня большие сомнения, что на SNMP не придется переписывать под циски, у джуна весьма загадочная трактовка MIB-ов.
И на Core и на DS найду все, что нужно, так как весь роутер, через который работает инет, тоже подключен к кору.Naves
14.08.2018 16:58У разных вендоров действительно разные MIB, но обычно уже все сделано до нас.
github.com/Cacti/plugin_mactrack/tree/develop/lib
Для Ubiquiti EdgeSwitch использовал настройки mactrack_trendnet
maxx_s
14.08.2018 17:04До тех пор пока железка которую вы ищете ходит в интернет.
alk0v Автор
14.08.2018 17:13Альтернатива — пройти по всем Access-свичам полным перебором?
maxx_s
14.08.2018 23:04Ну вы же не сказали для чего вы ищите порт, откуда берёте MAC и что делаете дальше.
Если из ARP, то можно ещё до начала поиска сделать ping. А перебор это не альтернатива, а скорее последний шанс, если не нашёл по вашей логике.
alk0v Автор
14.08.2018 16:38Тут, собственнно, рецепт
forums.juniper.net/t5/Ethernet-Switching/Use-SNMP-to-figure-out-which-port-a-MAC-address-is-reachable-on/m-p/238166/highlight/true#M10183
P.S. да, скорее всего он будет универсальным для разных вендоров, но то, что простым в реализации — не уверен.maxx_s
14.08.2018 17:07Зависит от библиотек, Вы ж не будете сами с нуля писать.
alk0v Автор
14.08.2018 17:11Библиотеки тут ни при чем, поддержка SNMP на питоне близка к идеалу, там все элементарно, в целом решение не выглядит простым.
То что в Cacti есть этот модуль — отлично, но мне нужно было не для Cacti.
ximeric
xgu.ru/xg-ids/ch04s02.html
Проверял метод из статьи по ссылке, вручную через snmpwalk на коммутаторах DLink — получение порта по SNMP вполне реально, универсально, и безопасный доступ только по чтению через public. Нужно только найти правильную базу для соответствующих OID.