Subversion Repositories sysadmin_scripts

Rev

Rev 25 | Blame | Last modification | View Log | Download | RSS feed

#! /usr/bin/env perl

# Script does an snmp walk through the ports of one or more network
# switch, gathering MAC addresses assigned to each port.
# 
# requires snmp running on this machine. Also uses YAML::Tiny perl module
# under debian and derivatives, use the following command to install
# apt-get -y install snmp libyaml-tiny-perl
#
# It then does an snmp walk through the arp table of one or
# more routers to determine IP and DNS entries for the MAC address
# 
# Information gathered is stored in persistent storage (a yaml formatted file
# in the same directory), then reloaded at the next start of the program.
# As new entries become available, they are added. A time stamp
# records the last time an entry was "seen"

# Data is stored in the hash %switchports with the following structure
# %switchports =
#     {switch} (from $config{'switches'} key)
#        {name} (scalar, from snmp)
#        {location} (scalar, from snmp)
#        {'ports'} (constant key for sub hash)
#           {port number} (from snmp)
#              {connection} (uniqe id from snmp, basically the MAC)
#                 {mac} (from snmp walk of switch)
#                 {ip}  (from snmp walk of router)
#                 {hostname} (from reverse dns query)
#                 {lastseen} (last time this was active as unix timestamp)

# 20190407 RWR
# converted to use external config file in YAML format
# added the ability to ignore ports on the switches
# 20190514 RWR
# Added port aliases
# 20190526 RWR
# fixed mapSwitchCSV.pl to output in various delimited format (see README)

use strict;
use warnings;
use Data::Dumper; # only used for debugging
use Socket; # for reverse dns entries
use YAML::Tiny; # apt-get libyaml-tiny-perl under debian
use File::Spec; # find location of script so we can put storage in same directory
use File::Basename;

our $VERSION = '1.2';

my %config;

# where the script is located
my $scriptDir = dirname( File::Spec->rel2abs( __FILE__ ) );
# put the statefile in there
my $STATEFILE = $scriptDir . '/mapSwitches.yaml';
my $CONFIGFILE = $scriptDir . '/mapSwitches.config.yaml';
# main hash that holds the data we collect
my %switchports;
# some OIDS we need for the program
my $SWITCHPORTMIB  = 'iso.3.6.1.2.1.17.4.3.1.2';
my $SWITCHMACMIB   = 'iso.3.6.1.2.1.17.4.3.1.1';
my $SWITCHIFNAMEMIB = '1.3.6.1.2.1.31.1.1.1.18';
my $ROUTERIPMACMIB = 'iso.3.6.1.2.1.4.22.1.2'; #'iso.3.6.1.2.1.3.1.1.2';
my $DEVICENAMEMIB  = '1.3.6.1.2.1.1.5';
my $DEVICELOCMIB   = '1.3.6.1.2.1.1.6';

sub makeMAC {
   my $string = shift;
   my @octets = split( '\.', $string );
   for ( my $i = 0; $i < @octets; $i++ ) {
      $octets[$i] = sprintf( "%02x", $octets[$i] );
   }
   return join( '', @octets );
}

sub updateValue {
   my ( $oldValue, $newValue ) = @_;
   $newValue = '' unless defined $newValue;
   if ( $newValue ) {
      $$oldValue = $newValue unless $newValue eq $$oldValue;
      return 1;
   }
   return 0;
}

sub getOneSNMPValue {
   my ( $oid, $community, $ip, $regex ) = @_;
   my $line = `snmpwalk -v1 -c $community $ip $oid`;
   $line =~ m/$regex/i;
   return $1;
}

sub initialize {
   my ( $hash, $name, @keys ) = @_;
   foreach my $key ( @keys ) {
      $hash->{$name}->{$key} = '' unless $hash->{$name}->{$key};
   }
}

sub updateIP {
   my ( $switchports, $ip, $mac ) = @_;
   foreach my $switch ( keys %$switchports ) {
      my $thisSwitch = $switchports->{$switch}->{'ports'};
      foreach my $port ( keys %$thisSwitch ) {
         my $thisPort = $thisSwitch->{$port};
         foreach my $connection ( keys %$thisPort ) {
            # skip the alias information on a port
            next if $connection eq 'alias';
            &initialize( $thisPort,$connection,'mac','ip','hostname','lastseen' );
            if ( $switchports->{$switch}->{'ports'}->{$port}->{$connection}->{'mac'} eq $mac ) {
               my $modified = &updateValue( \$switchports->{$switch}->{'ports'}->{$port}->{$connection}->{'ip'}, $ip );
               $modified |= &updateValue( \$switchports->{$switch}->{'ports'}->{$port}->{$connection}->{'hostname'},gethostbyaddr( inet_aton( $ip ), AF_INET ));
               $switchports->{$switch}->{'ports'}->{$port}->{$connection}->{'lastseen'} = time() if $modified;
               return;
            } # if we found it
         } # for connection
      } # for port
   } # for switch
} # updateIP

sub getAliases {
   my ($ip, $community, $MIB ) = @_;
   my %return;
   # get the aliases
   my $values = `snmpwalk -v1 -c $community $ip $MIB`;
   my @aliases = split( "\n", $values );
   chomp @aliases;
   foreach my $entry ( @aliases ) {
      $entry =~ m/\.(\d+) = STRING: "?([^"]*)"?$/;
      $return{$1} = $2;
   }
   return \%return;
}
   

if ( -e $CONFIGFILE ) {
   my $yaml = YAML::Tiny->read( $CONFIGFILE );
   %config = %{ $yaml->[0] };
} else {
   die "could not locate config file $CONFIGFILE\n";
}

# read the saved state into memory if it exists
if ( -e $STATEFILE ) {
   my $yaml = YAML::Tiny->read( $STATEFILE );
   %switchports = %{ $yaml->[0] };
}

# first, get all of the MAC/Port assignments from the switches
foreach my $switch ( keys %{$config{'switches'}} ) {
   &initialize( \%switchports, $switch, 'name','location' );
   &updateValue(
      \$switchports{$switch}{'name'},
      &getOneSNMPValue( $DEVICENAMEMIB,$config{'switches'}{$switch}{'community'},$switch, '= STRING: "?([^"]*)"?' )
      );

   &updateValue(
      \$switchports{$switch}{'location'},
      &getOneSNMPValue( $DEVICELOCMIB,$config{'switches'}{$switch}{'community'},$switch, '= STRING: "?([^"]*)"?' )
      );
   # get the MAC addresses
   my $values = `snmpwalk -v1 -c $config{switches}{$switch}{'community'} $switch $SWITCHPORTMIB`;
   my @lines = split( "\n", $values );
   my $aliases = getAliases ( $switch, $config{switches}{$switch}{'community'}, $SWITCHIFNAMEMIB );
   
   foreach my $line ( @lines ) {
      $line =~ m/$SWITCHPORTMIB\.([0-9.]+)\s=\sINTEGER:\s+(\d+)/;
      my $uuid = $1; # this is the ID string of this MAC; normally the MAC itself in decimal form
      my $port = $2; # this is the port number
      next unless $port; # skip port 0, or any port which has nothing
      next if $config{'switches'}{$switch}{'portsToIgnore'} && grep( /^$port$/, @{$config{'switches'}{$switch}{'portsToIgnore'}} );
      $switchports{$switch}{'ports'}{$port}{$uuid}{'mac'} = &makeMAC( $1 );
      $switchports{$switch}{'ports'}{$port}{'alias'} = $aliases->{$port} ? $aliases->{$port} : '';
   }
}

# die Dumper( \%switchports );


# Now, try to match up the MAC address. Read the ARP table from the router(s)
foreach my $router ( keys %{$config{'routers'}} ) {
   my $values = `snmpwalk -v1 -c $config{routers}{$router}{community} $router $ROUTERIPMACMIB`;
   my @lines = split( "\n", $values );
   foreach my $line ( @lines ) {
      $line =~ m/$ROUTERIPMACMIB\.([0-9]+)\.([0-9.]+)\s=\sHex-STRING:\s([0-9a-z ]+)/i;
      my $interface = $1;
      my $ip = $2;
      my $mac = lc $3;
      $mac =~ s/ //g;
      &updateIP( \%switchports, $ip, $mac );
   }
}

#die Dumper( \%switchports );

# save the state file for later, so we can find things which disappear from the network
my $yaml = YAML::Tiny->new( \%switchports );
$yaml->write( $STATEFILE );

#print Dumper( \%switchports );
1;


Generated by GNU Enscript 1.6.5.90.