Not so long ago, in connection with the expansion of the network administrators' staff, the growth in the number of equipment in the network and the range of tasks assigned to this equipment, there was a desire to track changes in its configuration and to keep a history of these changes.
The obvious solution was to store the configuration in a version control system. The choice fell on Subversion, also known as SVN.
I will not dwell on the installation and setup of the repository, as many pages on the Internet are devoted to this and there is nothing non-standard in this. Here I will describe my way of collecting configuration from the equipment and taking into account changes.
To begin with, we will create a working copy of our repository, where the configs will be collected by the perl script.
svn checkout svn://svn.reposerver.ru/configs /configs
Let's start writing a script. The script was written taking into account several features.
- simple addition / removal of devices in the list of scanned
- multivendor network
- notification of changes by mail
- notification of connection errors by mail
Initialization of variables:
#!/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"; ### .
Let's update and get the current revision number. It is useful for us to compare versions.
$_=`/usr/local/bin/svn update $path`; s/At revision (\d+)./$1/; $prev_rev = $_;
Lok / unlock functions Allow to run only one copy of the script.
sub lock($) { open(LK,">$_[0]") or return 0; flock(LK,LOCK_EX|LOCK_NB); } sub unlock($) { unlink($_[0]); flock(LK,LOCK_UN); close(LK); }
Next, read the configuration file. The file must be a set of lines. Each line contains the address of the piece of iron and its type, separated by a colon:
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]); } }
Here we create a file with a configuration for each line of the config (in case it was not available) and call a set of functions depending on the type of device. We will consider the functions later, let me just say that after they are completed, we will have a new config file from the device. Check if there are changes in the config. If there is, commit and set the flag for changes to 1. If a new host is added, we also add it to the repository. With the flag set, we are writing a letter.
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(); }
The main script is complete. Next, a set of functions.
For the function of collecting the config, in view of the multi-vendor nature, it was decided to use a more or less universal mechanism - a connection to the switch and a command to view the configuration.
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; } }
Methods of connecting and sending commands are executed in the context of eval, so that the script does not crash if there are problems with the connection. Just in case five connection attempts are given. Then either we write an error message (we set the flag in order for the error to go away by mail) or we write the resulting array to a file. Checking the length of the array is done because some devices periodically return messages like: āit is impossible to issue a configuration, try laterā when they are very busy with something so that such a message is not written to the file, because there is no obvious connection error, but there is also a config not. You can certainly parse the conclusion on the subject of keywords, but this test is simpler and more universal.
For a new type of device, a new function is added in which you can describe your own logic of working with this type. For example 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`; } }
In this example, instead of telnet, the config is downloaded via scp. The account for access to the shell with limited rights has been created on the device.
During the tests, it turned out that sometimes there are lines in the config that are constantly changing, but do not carry an important semantic load. For example, ntp clock-period in Cisco, or Extreme likes to add an asterisk to the prompt if there are unsaved changes. The function of cutting out such lines was added to ignore them and take into account only important changes:
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; }
After the passage of all devices from our list, if somewhere the flag of the need for a mail message was set, then we send a letter to which we include a list of changes, and a variable with errors. The list of changes is obtained by the standard svn diff, indicating the previous revision number.
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; }
At the end delete lockfile
&unlock("lockfile");
This script runs on the crown several times a day. All the changes come to the mail and at any time you can connect to any SVN client to watch the changes, compare versions, roll back, etc.
Iāll make a reservation that Iām not a pearl barley guru, and all comments and advice will be gratefully accepted.