Pull to refresh

Сбор конфигурации сетевого оборудования и хранение их в SVN

Reading time 5 min
Views 16K
Не так давно, в связи с расширением штата сетевых администраторов, роста числа оборудования в сети и круга задач, возлагаемых на это оборудование, возникло желание отслеживать изменение его конфигурации, и хранить историю этих изменений.
Очевидным решением стало хранение конфигурации в системе управления версиями. Выбор пал на Subversion, так же известную как SVN.
На установке и настройке репозитория останавливаться не буду, так как этому посвящено много страниц в интернете и ничего нестандартного в этом нету. Здесь опишу свой способ сбора конфигурации с оборудования и учет изменений.

Для начала создадим рабочую копию нашего репозитория, куда и будут скриптом на perl собираться конфиги.
svn checkout svn://svn.reposerver.ru/configs /configs

Приступим к написанию скрипта. Скрипт писался с учетом нескольких особенностей.
  • простое добавление/удаление устройств в списке проверяемых
  • мультивендорная сеть
  • уведомление об изменениях почтой
  • уведомление об ошибках подключения почтой

Инициализация переменных:
#!/usr/bin/perl -w 

use Net::Appliance::Session;
use Net::SCP::Expect;
use Switch;
use Fcntl ':flock'; 

exit(1) unless &lock("lockfile"); 

my $date = `/bin/date`;
my $flag=0; ### флаг наличия изменений
my $path="/configs"; ###путь к рабочей копии
my $cfg = "/configs/switches.cfg"; ### файл списка оборудования
my $error = "\n"; ### переменная сообщений об ошибке.

Сделаем update и получим номер текущей ревизии. Он нам пригодится для сравнения версий.
$_=`/usr/local/bin/svn update $path`;
s/At revision (\d+)./$1/;
$prev_rev = $_;

Функции лока/анлока. Позволяют запускать только одну копию скрипта.
sub lock($) {
	open(LK,">$_[0]") or return 0;
	flock(LK,LOCK_EX|LOCK_NB);
}
sub unlock($) {
        unlink($_[0]);
	flock(LK,LOCK_UN);
	close(LK);
} 

Далее читаем конфигурационный файл. Файл должен представлять из себя набор строчек. Каждая строчка содержит адрес железки и ее тип, разделенные двоеточием:
switch1:e
switch2:cat
router1:c
router2:j

open (CFG, "$cfg") || die "Cant open file $cfg: $!";
while (<CFG>) {
	next if /^\#/; ###Пропускаем комментарии в конфиге
        next if /^\s*$/; ###Пропускаем пустые строки в конфиге
	chomp;
	my @host = split /:/,$_;
	switch($host[1]) {
		case "cat" {
			`/usr/bin/touch $path/$host[0]`;
			&cat_telnet($host[0]);
			&str_remove ($host[0]);
		}
		case "c" {
			`/usr/bin/touch $path/$host[0]`;
			&cat_telnet($host[0]);
			&str_remove ($host[0]);
		}
		case "e" {
			 `/usr/bin/touch $path/$host[0]`;
		    	&extreme_telnet($host[0]);
		    	&str_remove ($host[0]);
		}
		case "j" {
		    &juni_scp($host[0]);
		}
	}

Здесь мы для каждой строчки конфига создаем файл с конфигурацией (на случай если его небыло) и вызываем набор функций в зависимости от типа устройства. Функции расмотрим позже, скажу лишь, что после их выполнения у нас будет новый собраный с устройства конфиг. Проверяем есть ли изменения в конфиге. Если есть — коммитим и устанавливаем флаг наличия изменений в 1. Если добавился новый хост, также добавляем его в репозиторий. При выставленном флаге – пишем письмо.
my $svnst = `/usr/local/bin/svn status $path/$host[0]`;
    	if ($svnst =~ /^M/) { ### Если файл был модифицирован
		$flag=1;
		`/usr/local/bin/svn commit -m "$date" $path/$host[0]`;	
	}
if ($svnst =~/^\?/) { ### Если добавился новый файл
		`/usr/local/bin/svn add $path/$host[0]`;
		`/usr/local/bin/svn commit -m "Add new host at $date" $path/$host[0]`;
	}
}
close CFG;
if ($flag==1) {
	&diff_mail();
}

Основной скрипт закончен. Далее набор функций.
Для функции сбора конфига ввиду мультивендорности, было решено использовать более или менее универсальный механизм – коннект на свитч и команда просмотра конфигурации.
sub cat_telnet {
	(my $host1)=@_;
	$try = 5;
	$update='false';
	while ($try>0) {
		my $s = Net::Appliance::Session->new(
			Host => $host1,
			Transport => 'Telnet',
			Timeout => 50,
			Source => '/configs/nas-pb.yml',
			Platform=> 'IOS'
		);
		eval {
			$s->do_login();
			$s->connect(
				Name => 'username',
				Password => 'pass',
			);
			my @temp = $s->cmd('show run');
		};
		if ($#temp>5) {
			$s->close;
			$update='true';
			last;
		}
		$try=$try-1 ;
		if ($try==0) {
			$error = $error.»Some error occure while getting config from «. $host1. «\n»;
			$flag=1;
		}
		$s->close;
	}
	if ($update eq 'true'){
		open (CFG1, «>$path/$host1») || die «Cant open file $path/$host1: $!»;
		print CFG1 @temp;
		close CFG1;
	}
}

Методы подключения и передачи команд выполняются в контексте eval, чтобы скрипт не вылетал если будут проблеммы с подключением. На всякий случай дается пять попыток подключения. Далее либо записываем сообщение об ошибке,(флаг выставляем, для того чтобы ошибка ушла почтой) либо записываем полученый массив в файл. Проверка длины масива сделана потому, что некоторые устройства переодически возвращают сообщения вида: «невозможно выдать конфигурацию, попробуйте позже», когда они очень чем-то заняты, что бы такое сообщение не записывалось в файл, потому как явной ошибки подключения нет, но конфига тоже нет. Можно конечно парсить вывод на предмет ключевых слов, но данная проверка проще и универсальней.
Для нового типа устройства добавляется новая функция в которой можно описать собственную логику работы с данным типом. Например Juniper:
sub juni_scp {
	(my $host1)=@_;
	$try = 5;
	$update = 'false';
	while ($try>0) {
		my $s = Net::SCP::Expect->new(
			user=>'username',
			password=>'pass',
			auto_yes=>1
		);
		eval {
			my @temp= $s->scp("$host1:/config/juniper.conf.gz","$path/$host1.gz" );
		};
		if ($#temp eq "0") {
			$update='true';
			last;
		}
		$try=$try-1 ;
		if ($try==0) {
			$error = $error."Some error occure while getting config from ". $host1. "\n";
			$flag=1;
		}
	}
	if ($update eq 'true'){
		`/usr/bin/gunzip -f $path/$host1.gz`;
	}
}

В данном примере вместо telnet конфиг скачивается по scp. На устройстве создана учетка для доступа в шелл с ограниченными правами.

В процессе тестов выяснилось, что иногда попадаются строчки в конфиге, которые постоянно меняются, но не несут важной смысловой нагрузки. Например ntp clock-period в Cisco, или Extreme любит добавлять звездочку в приглашение, если есть несохраненные изменения. Была добавлена функция вырезания таких строчек, что бы игнорировать их и учитывать только важные изменения:
sub str_remove {
	(my $host)=@_;
	open (F,"$path/$host") ; 
	my @f = <F>; 
	close F; 
	open (F, ">$path/$host"); 
	foreach $str (@f) { 
   		print F $str if ( $str !~ /^ntp clock-period/  and $str !~ /^Current configuration/ and $str !~ /^Slot-/ and $str !~ /^\* Slot-/ and $str !~ /^\*/); 
	} 
	close F;  
}

После прохода всех устройств из нашего списка, если где-то был выставлен флаг необходимости почтового сообщения, то отправляем письмо в которое включаем список изменений, и переменную с ошибками. Список изменений получаем стандартным svn diff, указывая предыдущий номер ревизии.
sub diff_mail {
	@mail = `/usr/local/bin/svn diff $path -r $prev_rev`;
	open (MAIL, "| /usr/sbin/sendmail user@mydomain.ru, user2@mydomain.ru ")  || die " $!";
	print MAIL <<EOF;
From: Config Checker
Subject: Config Update

@mail
$error

EOF
	close MAIL;
}

В конце удаляем локфайл
&unlock("lockfile");

Данный скрипт запускается по крону несколько раз в день. Все внесенные изменения приходят на почту и в любой момент можно любым SVN клиентом подключаться наблюдать за изменениями, сравнивать версии, откатываться и т.д.
Оговорюсь, что я не гуру перлового программирования, и все замечания и советы будут с благодарностью приняты.
Tags:
Hubs:
+23
Comments 24
Comments Comments 24

Articles