Поиск коммутатора и порта по мак адресу хоста

30 Дек
2011

Всем доброго дня.

Прежде всего, моя статья адресована администраторам крупных корпоративных сетей (построенных исключительно на оборудовании Cisco), в которых может насчитываться до 16000 ethernet портов. В таких сетях многие, казалось бы, элементарные вещи начинают со временем сильно утомлять.

К примеру, в крупных компаниях многим пользователям почему-то не сидится на местах и они начинают мигрировать с кабинета в кабинет, с этажа на этаж…, а это значит, что их хосты подключается в новые розетки и не факт, что эти пользователи после переезда окажутся в своем VLAN. Это в свою очередь влечет за собой следующее: почти каждый день в Service Desk приходят наряды такого содержания: «Просьба перевести такой-то MAC адрес в таком-то здании в такую-то сеть».

И вроде бы мелочь, но если она повторяется каждый день, то наступает момент, когда количество переходит в качество и подобные наряды ничего кроме раздражения не вызывают. Не знаю как остальные администраторы, но я терпеть не могу рутину в любом ее проявлении. Поэтому написал полезную утилиту на перле: fport.pl.

Пока в утилите минимальный функционал: она ищет порт и коммутатор по мак адресу и текущий VLAN порта. Утилита найденный порт не переводит в другой VLAN (пока я тестирую данную утилиту на предмет багов, т.к. в коде не всегда получается предусмотреть заранее все нюансы, а если случайно утилита переведет транковый порт… в общем не очень кошерно).

Формат задания аргументов для утилиты следующий:
./fport.pl “object1|mac1,mac2,mac3…” “object_2|mac1,mac2…” …

Формат задания MAC адреса: 2 последних байта либо все 6 байт, разделенных точками через каждые 2 байта; регистр не важен, между байтами могут быть дефисы или двоеточия.

Примеры:
0019.dbab.d5cf
00:19.db:ab.d5:cf
00-19.db-ab.d5-cf
001D.928A.9CD0
00:1D.92:8A.9C:D0
00-1D.92-8A.9C-D0
0c54
0c:54
0c-54
8EF3
8E:F3
8E-F3

Пример вывода (настоящие значения заменены псевдозначениями):
./fport.pl «XXX|zz:zz,nn:nn» «YYYY|mm-mm.mm-mm.mm-mm»
Searching MAC in the bulding block XXX:
MAC zzzz not found!
MAC nnnn found on device: x.x.x.x on port: Gix/y/z, now MAC in VLAN: xxx.
Searching MAC in the bulding block YYYY:
MAC mmmm.mmmm.mmmm found on device: y.y.y.y on port: Gix/y/z, now MAC in VLAN: yyy.

Напутствия:

Перед использованием данной утилиты должна быть создана база данных как минимум с тремя таблицами:
1. init содержит соответствие: id_объекта – ip_адрес_distribution:

init:
+———+———+
| obj_id | dev_ip |
+———+———+
| 1 | 3.3.3.3 |
| 2 | 2.2.2.2 |
+———+———+

2. aliases содержит список различных псевдонимов объекта, т.к. на предприятии коллеги часто могут использовать не только формально название объекта, но и всевозможные сокращения: id_объекта – псевдоним_объекта:

aliases:
+———+————-+
| obj_id | alias |
+———+————-+
| 1 | Mira |
| 2 | Vernadskogo |
| 2 | Vern |
+———+————-+

3. objects содержит соответствие: id_объекта – название_объекта:

objects:
+———+————————-+
| obj_id | object |
+———+————————-+
| 1 | Prospekt Mira, 101 |
| 2 | Prospekt Vernadskogo, 5 |
+———+————————-+

Также должен работать протокол CDP на всех транках.

Немного о принципе работы: fport.pl выделяет аргументы, по очереди выбирает из базы данных необходимые ip адреса соответствующих distribution устройств и, используя данные какой-либо учетный записи в TACAS, по очереди ищет все маки на данном объекте. Процесс поиска представляет собой рекурсивный парсинг вывода следующих команд: sh mac- mac, sh cdp neighbors port_name detail, sh etherchannel summary | i Po_number (вывод команды sh etherchannel summary используется для случая, когда на порту настроена агрегация).

Более подробно с алгоритмом работы можно ознакомиться из исходного кода под катом (в коде присутствуют основные комментарии).

Вот собственно сам код.

Ставьте perl, mysql создавайте базу и таблицы с описанным выше интерфейсом и все будет ok:

#!/usr/bin/perl
use strict;
use DBI;
#Чтобы подключатся к устройствам Cisco необходим этот модуль.
use Net::Telnet::Cisco;

our ($acs_login, $acs_password, $db_name, $db_login, $db_password, $dbh);
#Инициализируем логины и пароли.
&init_accounts;
#Создем подключение к нашей базе.
$dbh = &db_connect ($db_name, $db_login, $db_password);
#По очереди обрабатываем перечисленные объекты.
foreach my $arg (@ARGV) {
my @MAC = ();
(my $object, my $MAC, my $dev_ip) = (undef, undef, undef);
($object, $MAC) = split(/\|/, $arg);
@MAC = split(/,/, $MAC);
#Извлекаем из базы ip дистрибьюшена.
$dev_ip = &get_dev_ip($object);
print "Searching MAC in the bulding block $object:\n";
foreach my $mac (@MAC) {
#удаляем ненужные символы и преобразуем MAC к нижнему регистру.
$mac =~ tr/ABCDEF/abcdef/;
$mac =~ s/[-:]//g;
#Начинаем рекурсивный поиск MAC адреса, передаем этой функции ip дистрибьюшена и искомый MAC адрес.
(my $ip, my $port, my $current_vlan) = &find_port_by_mac ($dev_ip, $mac);
if ($ip and $port and $current_vlan) {
print " MAC $mac found on device: $ip on port: $port, now MAC in VLAN: $current_vlan.\n";
}
else {
print " MAC $mac not found!\n";
}
}
}

print "\n\nCompleted!\n";

sub init_accounts {
$acs_login = 'ram';
$acs_password = 'kexdhftuj';

$db_name = 'Lukoil';
$db_login = "root";
$db_password = "1234567890";
}

sub db_connect {
my ($db_name, $db_login, $db_password, $data_source, $dbh);
($db_name, $db_login, $db_password) = @_;
$data_source = "DBI:mysql:$db_name:localhost";
$dbh = DBI->connect($data_source, $db_login, $db_password);
return $dbh;
}

sub get_dev_ip {
my ($object, $dev_ip, $subquery, $request, $sth, $ref);
($object) = @_;
$subquery = "select obj_id from aliases where alias in ('$object')";
$request = "select obj_id,dev_ip from init where obj_id in ($subquery) group by obj_id;";
$sth = $dbh->prepare($request);
$sth->execute();
$ref = $sth->fetchall_arrayref();
$dev_ip = ${${$ref}[0]}[1];
return $dev_ip;
}

sub find_port_by_mac {
my ($session, $dev_ip, $ip, $int, $vl, $port, $vlan, $neighbor_ip, $sh_mac, $mac);
($dev_ip, $mac) = @_;
#Подключаемся к дистрибьюшену
if (eval {$session = Net::Telnet::Cisco->new(Host=>"$dev_ip", Timeout =>"25"); $session->login(Name => "$acs_login", Password => "$acs_password");}) {
#получаем информацию о том, за каким портом виден данный MAC.
($port, $vlan) = &get_port_to_neighbor($session, $mac);
#получаем информацию о том, за каким портом виден данный MAC.
if ($port) {
#Смотрим, виден ли коммутатор по CDP за этим портом или нет, если виден, то рекурсивно идем на него и т.д.
$neighbor_ip = &get_neighbor_ip($session, $port);
if ($neighbor_ip) {
($dev_ip, $port, $vlan) = &find_port_by_mac($neighbor_ip, $mac);
}
}
$session->close;
return ($dev_ip, $port, $vlan);
}
}

sub get_port_to_neighbor {
my ($session, $vlan, $mac, $sh_mac, $port, $Po, $etherchannel);

($session, $mac) = @_;
if (eval {($sh_mac) = $session->cmd("sh mac- | i $mac");}) {
$sh_mac =~ s/^(\n+)//;
$sh_mac =~ s/(\n+)$//;
$sh_mac =~ s/^(\s+)//;
$sh_mac =~ s/(\s+)$//;
if ($sh_mac =~ /(\d{1,3})\s+([a-f0-9]{4}\.){2}[a-f0-9]{4}.+([A-Za-z]{2})\s*((\d+\/)+\d+)/) {
if ($1) {$vlan = $1;}
if ($3 and $4) {$port = $3.$4;}
}
elsif ($sh_mac =~ /(\d{1,3})\s+([a-f0-9]{4}\.){2}[a-f0-9]{4}.+(Po\d+)/) {
if ($1) {$vlan = $1;}
if ($3) {$Po = $3;}
if (eval {($etherchannel) = $session->cmd("sh etherchannel summary | i $Po");}) {
$etherchannel =~ /([A-Za-z]{2}\s*(\d+\/)+\d+)/;
if ($1) {$port = $1;}
}
}
return ($port, $vlan);
}
}

sub get_neighbor_ip {
my (@sh_cdp_n_detail, $session, $sh_cdp_n_detail, $port, $neighbor_ip, $ip_phone);
($session, $port) = @_;
$ip_phone = 1;
if (eval {@sh_cdp_n_detail = $session->cmd("sh cdp neighbors $port detail");}) {
foreach $sh_cdp_n_detail (@sh_cdp_n_detail) {
$sh_cdp_n_detail =~ s/^(\n+)//;
$sh_cdp_n_detail =~ s/(\n+)$//;
$sh_cdp_n_detail =~ s/^(\s+)//;
$sh_cdp_n_detail =~ s/(\s+)$//;
if ($sh_cdp_n_detail =~ /[Ii][Pp]\s*address\s*:\s*((\d{1,3}\.){3}\d{1,3})/){
if ($1) {$neighbor_ip = $1;}
}
#Убеждаемся, что соседнее устройство не IP телефон
elsif ($sh_cdp_n_detail =~ /SEP|SIP/){
$ip_phone = 0;
}
}
if ($ip_phone) {return $neighbor_ip;}
else {return 0;}
}
}
По материалам Хабрахабр.



загрузка...

Комментарии:

Наверх