#!/usr/bin/perl -w use strict ; use SNMP ; # use interface to net-SNMP (was UCD-SNMP) package use Time::Local ; # convert hh:mm:ss etc to unixtime use lib '/usr/local/myNMS/bin' ; use My_Utils qw(ping NOW $MINUTE $DAY YYYY_MM_DD_hh_mm_ss nameDate); use My_DButils ; #use My_Config ; my $VERSION="14.1"; =pod Copyright (c) 2000-2001 John Stumbles and the University of Reading. All rights reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. C H A N G E H I S T O R Y --------------------------- 14.2 31Jan01 use Modules use Log_Event in My_DButils 14.1 25Jan01 changed LOG sub to use myNMS{LOG} filepath; declared 'my myNMS_DIRS' before BEGIN{} block so that values exist in rest of script 14.0 23jan01 accomodate new directory structure 13.1.2 22Jan01 added test that we have something to pass to update_device_dot1d_table() from update_all_dot1d() 13.1.1 22Jan01 cosmetic: added \n to INFORM "no IP address found for device $device\n" ; 13.1 20Jan01 update_all_dot1d() INFORMs and skips devices for which it finds no IP address 13.0.1 20Jan01 yet another check added that and IP address is being passed to update_device_snmp when it is called 13.0 10Jan01 Added checks of sysObjectID and sysServices to SNMP checking (update_device_snmp) routine 12.1 09Jan01 renamed from netinfo, tidied up a bit and moved to myNMS directory most subs now die instead of returning silently if called without required parameters 12.0 30Nov00 added -u|--until {time} option and changed default action to exit at midnight using function time_to_run which returns 0 at/after given exit time 11.2 23Oct00 corrected typo in creup_ARP_record updating IP_MAC where IPadd = 'IP' instead of '$IP'! 11.1 20Oct00 creup_ARP_record modified for dropped deviceID field from IP_MAC table 11.0 19Oct00 new CLI option --MAC directly invokes update_MAC_connectipns --dot1d / do_dot1d() without list now updates dot1d tables for all switches seen, then does update_MAC_connections 10.2 21Sep00 cosmetic: when called with hashref (as in auto mode) update_device_dot1d_table only does INFORM if VERBOSE 10.1 21Sep00 cosmetic: INFORM prints Version instead of full path to prog 10.0 21Sep00 dot1d table now has nMACs field (no of MACs seen on device port at time this MAC was seen) update_device_dot1d_table and update_device_arp_table modified to count MACs for this field sub update_dot1d_record replace by inline do->("REPLACE ....) into table bug fixed: update_all_dot1d wasn't updating dot1d, trying to update_device_snmp first which wasn't returning true - modified to have update_device_snmp return ref to %SNMP and update_device_dot1d_table() take this hashref (as well as deviceID or IPaddress) get_device_snmp_interfaces modified to return if we already have interfaces ({ifTable}) so it doesn't get this group a second time 0.9.2 21Sep00 snmp get of ifNames broken up into chunks of no more than some arbitrary value set by $MAX_SNMP_GET_VARS in order to eliminate '(tooBig)' error responses. Not sure if this will fix 'Timeout' and '(genError)' errors:- netinfo (/home/sunm2/scinfo/DB/netinfo): got error Timeout getting ifNames from device 134.225.114.1 suts1.rdg.ac.uk netinfo (/home/sunm2/scinfo/DB/netinfo): got error (tooBig) Response message would have been too large. getting ifNames from device 134.225.145.252 Physics_Gnd_Flr netinfo (/home/sunm2/scinfo/DB/netinfo): got error Timeout (parse int: message overflow: 35 len + 2 delta > 35 len) getting ifNames from device 134.225.145.194 sulafac1 netinfo (/home/sunm2/scinfo/DB/netinfo): got error Timeout (parse int: message overflow: 35 len + 2 delta > 35 len) getting ifNames from device 192.100.154.60 netinfo (/home/sunm2/scinfo/DB/netinfo): got error Timeout getting ifNames from device 192.133.244.56 CS_56X_1 netinfo (/home/sunm2/scinfo/DB/netinfo): got error (genError) A general failure occured getting ifNames from device 134.225.198.202 SXPS2 no: - netinfo --snmp - got error Timeout (parse int: message overflow: 35 len + 2 delta > 35 len) getting 1 ifNames from device 134.225.100.71 got error Timeout getting 2 ifNames from device 134.225.114.1 suts1.rdg.ac.uk got error Timeout (parse int: message overflow: 35 len + 2 delta > 35 len) getting 1 ifNames from device 192.100.154.60 got error (genError) A general failure occured getting 1 ifNames from device 134.225.198.202 SXPS2 got error (genError) A general failure occured getting 1 ifNames from device 134.225.198.203 SXPS3 got error Timeout (parse int: message overflow: 35 len + 2 delta > 35 len) getting 1 ifNames from device 134.225.198.212 got error Timeout (parse int: message overflow: 35 len + 2 delta > 35 len) getting 1 ifNames from device 134.225.192.24 snprin2 0.9.1 20Sep00 added to snmp error messages 0.9.0 20Sep00 (had introduced new field max_MACs to SNMP_ifTable, and new sub update_maxMACs, but backed off from this) get_device_snmp_system, get_device_snmp_interfaces and get_device_snmp_ipAdEnt modified to take (ref to) %SNMP structure containing $sess handle (and IPaddr) and add their own info to it: latter 2 functions will call get_device_snmp_system if it has not already been done update_device_snmp changed to accomodate DB SNMP info in same structure as SNMP info retrieved 'live' from device get_device_snmp also takes hashref to SNMP & adds fields dot1d and arp functions modified correspondingly update_DB_SNMP and create_DB_SNMP modified to take single SNMP structure in get_device_snmp_interfaces, calculation of ifLastChangeAt corrected 0.8.2 17sep00 Accelar Name fields trimmed from 'Slot s, Port p' to s/p 0.8.1 15Sep00 Name field expanded to 20 chars (in DB), get_device_snmp_interfaces now gets ifName into this. 0.8.0 15Sep00 extra field 'Name' (char(8)) introduced into SNMP_ifTable, and --snmp functions modified to fill this field, at present just with 8 char substr of ifDescr but intended to be modified to hold a proper description of the interface, e.g. at present Accelar's ifDescr is just 'baseTX' etc, but from ifMIB.ifMIBObjects.ifXTable.ifXEntry.ifName we get e.g. 'Slot 1, Port 1' etc Corresponding changes made to table structure through mysql, and in DB_utils 0.7.3 15Sep00 snmpwalk of ipAdEnt table now handled in new sub get_device_snmp_ipAdEnt() 0.7.2 15Sep00 --dot1d now skips 'self(4)' records (device's own MAC address) snmpwalk of interfaces table now handled in new sub get_device_snmp_interfaces() does this bit 0.7.1 15Sep00 minor change: sysObjIDtxt now set to '' instead of same as sysObjectID 0.7.0 14Sep00 sub update_device_arp_table now updates DB dot1d table (as well as arp) Hash %Device_Type is used to lookup from sysObjectID the identity of the device and determine if it is an Accelar in which case the Index value associated with the Phys + Net addresses is >> 16 to get the Accelar's proper ifIndex: it is assumed that any other device will do the right thing vis-a-vis index values, but the only other devices we have are 2 ciscos (7200 and 2500) so this should be checked for other routing devices. Also running --arp on any host with an arp cache will add to the arp and dot1d tables - not sure if we want this. 0.6.2 14Sep00 removed superfluous 'got error: Timeout from device' messages in --snmp 0.6.1 14Sep00 to fix problem that all process get stuck if, for whatever reason, no (recent) hosts can be found arp cache, the --snmp option now takes a --gateway parameter which it uses to prime discovery if necessary 0.6.0 11Sep00 converted everything which used SNMP to use SNMP.pm N O T E S --------- See Implementation.html =cut my $DB = 'NMS'; my $dbh = DB_init($DB) ; # (this is a copy of the database handle created in DB_utils) my ($PROGNAME)=$0=~/(?:.*\/)?([^\/]+)/; sub WARNING { print STDERR "$PROGNAME ($0): "; printf STDERR @_ ; } sub INFO { printf STDERR @_ ; } sub INFORM { INFO "$PROGNAME (VER $VERSION) at %s: ", YYYY_MM_DD_hh_mm_ss(time()); INFO @_ ; } # how many variables can we fetch with an SNMP get? # not sure: on a HP4000M ifNames, 64 seems to be the magic number my $MAX_SNMP_GET_VARS=64 - 1; my %Device_Type = ( # patterns to match to determine type of object from sysObjectID '.1.3.6.1.4.1.11.2.3.7.11.3' => ['HP', 'J3245A Switch 800T' ], '.1.3.6.1.4.1.11.2.3.7.11.6' => ['HP', 'J3299A Switch 224M' ], '.1.3.6.1.4.1.11.2.3.7.11.7' => ['HP', 'J4110A Switch 8000M' ], '.1.3.6.1.4.1.11.2.3.7.11.8' => ['HP', 'J4120A Switch 1600M' ], '.1.3.6.1.4.1.11.2.3.7.11.9' => ['HP', 'J4121A Switch 4000M' ], '.1.3.6.1.4.1.11.2.3.7.11.10' => ['HP', 'J4122A Switch 2400M' ], '.1.3.6.1.4.1.11.2.3.7.11.11' => ['HP', 'J4093A Switch 2424M' ], '.1.3.6.1.4.1.2272.8' => ['Nortel', 'Accelar 1200' ], '.1.3.6.1.4.1.9.1.108' => ['Cisco', '7200' ], '.1.3.6.1.4.1.9.1.217' => ['Cisco', 'Catalyst 2900' ], '.1.3.6.1.4.1.9.1.19' => ['Cisco', '2500' ], ) ; my $t_SNMP_update = 5 * $MINUTE ; # update SNMP info if last update more than this old (secs) my $t_SNMP_outdate = 2 * $DAY ; # treat SNMP info as out of date if more than this old (secs) [ 2 days] my $USAGE=qq($PROGNAME Version $VERSION Usage: This script is intended to be invoked 3 times: once with each option, (snmp, arp and dot1d) to continuously gather arp and dot1d tables and check SNMP info/discover new SNMP devices. Options common to all modes: $PROGNAME [-v|--verbose] [-c|--continuous] Verbose prints information as the program runs Continuous causes the program to run continuously instead of exiting at (or after) midnight Use: $PROGNAME [-g|--gateway ggg.ggg.ggg.ggg] [--snmp [device(s)]] gets SNMP info for specified device(s) or, without device(s) specified, runs continuously. If a gateway is specified this will be used when no hosts are available from arp cache table in DB, so this form can be used for priming snmp discovery $PROGNAME [--dot1d|--arp [device(s)]] gets arp/dot1d tables for specified device(s) or, without device(s) specified, runs continuously updating tables. $PROGNAME device(s) for pump-primimg: this gets SNMP info for specified device (typically local gateway) then does arp on device to get other hosts (including other gatways) then does SNMP on all hosts thus found, after which program should be self-sustaining. ); my $VERBOSE=0; my $GATEWAY ; my $UNTIL ; { my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday) = localtime (time); $UNTIL = timelocal (0, 0, 0, $mday, $mon, $year+1900) + $DAY ; } my $arg; while ($arg=shift) { if ($arg =~ /^-c|--continu/i) { $VERBOSE=1; $UNTIL = 0 ; next; } if ($arg =~ /^-v|--verbose/i) { $VERBOSE=1; next; } if ($arg =~ /^--dot1d/i) { do_dot1d(@ARGV); exit; } if ($arg =~ /^--MAC/i) { update_MAC_connections(); exit; } if ($arg =~ /^--arp/i) { do_arp(@ARGV); exit; } if ($arg =~ /^-g|--gateway/i) { die "ERROR -g or --gateway option must be followed by IP address of gateway\n" unless (($GATEWAY = shift) and ($GATEWAY =~ /^\d+\.\d+\.\d+\.\d+$/)) ; # IP address next ; } if ($arg =~ /^--snmp/i) { do_snmp(@ARGV); exit; } # elsif ($arg =~ /^--CONVERT/i) { convert_dot1d(); exit; } if ($arg =~ /^-h|--help/i) { die "$USAGE\n"; } } die $USAGE; ############################################################################## sub time_to_run() { return 1 unless $UNTIL ; # always true if UNTIL == 0 == continuous return $UNTIL > time() ; } ############################################################################## sub do_dot1d { # get dot1d tables from switches my @devs = @_; my $IPadd; if (scalar @devs) # if devices specified on command line: { $VERBOSE=1; # do verbose Log_Event ("$PROGNAME do_dot1d: invoked from cli for: " . (join ', ', @devs) ); foreach $IPadd (@devs) { if ($IPadd =~ /^\d+\.\d+\.\d+\.\d+$/) { update_device_dot1d_table($IPadd) ; next ; } WARNING "function do_dot1d: device [%s] not an IP address - ignoring\n", $IPadd ; } return ; } # (else) if no devs specified try all on network INFO "$PROGNAME $VERSION --dot1d: auto-poll mode\n"; Log_Event "$PROGNAME $VERSION do_dot1d: auto-poll mode"; while (time_to_run()) { update_all_dot1d() ; update_MAC_connections() ; } } ############################################################################## sub update_all_dot1d # get list of all devices with dot1d tables and update them { # get dot1d MAC tables from specified bridge/switch devices my @devs = @_; @devs = get_SNMP_sysServices(2) unless (scalar @devs); # get l3 devs die "$PROGNAME: get_dot1d: found 0 devices with sysServices == 2\n" unless scalar @devs; INFORM sprintf "get_dot1d: checking %s devices\n", scalar(@devs); Log_Event sprintf "$PROGNAME $VERSION update_all_dot1d: checking %s devices", scalar(@devs); my $device; # IP address or ID of device foreach $device (@devs) { my $IPadd = device_access_IP($device) ; unless ($IPadd) { INFORM "no IP address found for device $device\n" ; next ; } my $SNMP=update_device_snmp($IPadd, 0) ; unless ($SNMP->{DB}{deviceID}) { WARNING "update_device_snmp($IPadd, 0) did not return a ref to SNMP hash\n"; next ; } # (else) update_device_dot1d_table($SNMP) ; } } ################################################################################################### sub do_arp { # get arp tables from routers my @devs = @_; my $IPadd; if (scalar @devs) # if devices specified on command line: { $VERBOSE=1; # do verbose Log_Event ("$PROGNAME do_arp: invoked from cli for: " . (join ', ', @devs) ); foreach $IPadd (@devs) { if ($IPadd =~ /^\d+\.\d+\.\d+\.\d+$/) { update_device_arp_table($IPadd) ; next ; } WARNING "function do_arp: device [%s] not an IP address - ignoring\n", $IPadd ; } return ; } # (else) if no devs specified try all on network INFO "$PROGNAME $VERSION --arp: auto-poll mode\n"; Log_Event "$PROGNAME $VERSION do_arp: auto-poll mode"; while (time_to_run()) { # get_arp() ; update_all_arp() ; } } ################################################################################################### sub update_all_arp # get list of all devices with arp tables and update them { # get ARP tables from specified router-type devices my @devs = @_; @devs = get_SNMP_sysServices(6) unless (scalar @devs); # get l3 devs die "$PROGNAME: get_arp: found 0 devices with sysServices == 6\n" unless scalar @devs; INFORM sprintf "get_arp: checking %s devices\n", scalar(@devs) ; Log_Event sprintf "$PROGNAME $VERSION update_all_arp: checking %s devices", scalar(@devs) ; my $deviceIP; # name or address of device foreach $deviceIP (@devs) { update_device_arp_table($deviceIP) ; } } ################################################################################################### sub do_snmp { # get snmp system info from devices my @devs = @_; my $IPadd; if (scalar @devs) # if devices specified on command line: { $VERBOSE=1; # do verbose Log_Event ("$PROGNAME $VERSION do_snmp: invoked from cli for: " . (join ', ', @devs) ); foreach $IPadd (@devs) { if ($IPadd =~ /^\d+\.\d+\.\d+\.\d+$/) { update_device_snmp($IPadd, 0) ; # mtime == 0 forces update next ; } WARNING "function do_snmp: device [%s] not an IP address - ignoring\n", $IPadd ; } return ; } # (else) if no devs specified try all on network INFO "$PROGNAME $VERSION --snmp: auto-poll mode\n"; Log_Event "$PROGNAME $VERSION do_snmp: auto mode"; while (time_to_run()) { my $tlast = 900; # how recently my $t = time() - $tlast ; # look for hosts seen recently my $sth = $dbh->prepare("SELECT IPadd, time2 FROM IP_MAC WHERE time2 > $t ORDER BY time2 DESC") ; $sth->execute; if ($sth->rows) # if none?! { INFO "checking %s hosts\n", $sth->rows if $VERBOSE; my $IPadd; $sth->bind_col (1, \$IPadd); while ($sth->fetch) { die "IPadd retrieved from IP_MAC is null" unless $IPadd ; update_device_snmp($IPadd) ; } } else { die "$PROGNAME: do_snmp: no hosts found up in last $tlast secs and no gateway specified\n" unless $GATEWAY ; INFO "$PROGNAME $VERSION --snmp: auto-poll mode: no hosts found up in last $tlast secs, trying gateway $GATEWAY\n"; Log_Event "$PROGNAME $VERSION do_snmp: auto mode: no hosts found up in last $tlast secs, trying gateway $GATEWAY"; die "$PROGNAME: do_snmp: no SNMP info from gateway $GATEWAY specified\n" unless update_device_snmp($GATEWAY, 0) ; die "$PROGNAME: do_snmp: no ARP info from gateway $GATEWAY specified\n" unless update_device_arp_table($GATEWAY) ; } } } ################################################################################################### sub update_device_snmp # given an IP address, get or update SNMP info about device # optional update-unless time parameter, if 0 means update anyway # (so calling with ($IPadd, 0) forces update) { my ($IPadd,$tupdate) = @_ ; $IPadd or die "update_device_snmp called without IPadd"; $tupdate = $t_SNMP_update unless defined $tupdate; INFO "\nSNMPcheck $IPadd\n" if $VERBOSE; # do we already have record of device with this IP? my %SNMP = ( IPadd => $IPadd ) ; %{$SNMP{DB}} = db_SNMPsystem_by_IPadd($IPadd) ; # do no more if device already exists in DB and was updated recently enough, # and we're not being asked to update it anyway (with $tupdate==0): return 1 if ($SNMP{DB}{deviceID} and $tupdate and (time - $SNMP{DB}{mtime}) < $tupdate) ; # (else) attempt to get SNMP info from device unless (ping($IPadd)) # don't bother unless device is pingable! { # *** if we want to monitor devices this is where we should flag a problem *** INFO "\t- not responding to ping, giving up\n" if $VERBOSE; return 0 ; } unless (get_device_snmp(\%SNMP)) { # *** if we want to monitor devices this is where we should flag a problem *** INFO "\t- no SNMP response, giving up\n" if $VERBOSE; return 0 ; } if ($SNMP{DB}{deviceID}) # if we found this device in the DB ... { # if it is the same type of device: if ($SNMP{DB}{sysObjectID} eq $SNMP{sysObjectID} and $SNMP{DB}{sysServices} eq $SNMP{sysServices}) { # update its record INFO "\t- found in database, same sysObjectID and sysServices: updating\n" if $VERBOSE; return update_DB_SNMP(\%SNMP) ; } else { INFO "\t- different device found in DB - " . (($SNMP{DB}{sysObjectID} eq $SNMP{sysObjectID}) ? '' : "sysObjectID in DB: $SNMP{DB}{sysObjectID} NOW: $SNMP{sysObjectID}") . (($SNMP{DB}{sysServices} eq $SNMP{sysServices}) ? '' : "sysServices in DB: $SNMP{DB}{sysServices} NOW: $SNMP{sysServices}") . "\n" if $VERBOSE ; } } # (otherwise) # try to determine if it is an existing device with a changed IP address if ($SNMP{sysName}) { # look in SNMP for existing device with sysName = $SNMP{sysName} # %{$SNMP{DB}} = dbSNMP_by_WHERE("WHERE sysName=" . $dbh->quote($SNMP{sysName})); %{$SNMP{DB}} = dbSNMP_by_WHERE(sprintf "WHERE sysName=%s AND sysObjectID='%s' AND sysServices=%u" , $dbh->quote($SNMP{sysName}), $SNMP{sysObjectID}, $SNMP{sysServices} ); if ($SNMP{DB}{deviceID}) # if found, { return update_DB_SNMP(\%SNMP) ; } INFO "\t- not found in database\n" if $VERBOSE; } # (else) INFO "\t- creating new DB records:\n" if $VERBOSE; return create_DB_SNMP(\%SNMP) ; } ############################################################################## sub create_DB_SNMP { # my $SNMP = shift or return 0 ; my $SNMP = shift or die "create_DB_SNMP called without SNMP hashref"; # create deviceID from time $SNMP->{DB}{deviceID} = $SNMP->{mtime} ; $SNMP->{DB}{sysObjectID} = '' ; return update_DB_SNMP($SNMP) ; } ############################################################################## sub update_DB_SNMP # update database with SNMP info { # my $SNMP = shift or return 0 ; my $SNMP = shift or die "update_DB_SNMP called without SNMP hashref"; my %EVENT_INFO ; my $deviceID = $SNMP->{DB}{deviceID} or return undef ; # *warn* my $INFO = sprintf " deviceID %s sysName %s sysDescr %s sysContact %s sysLocation %s sysObjectID %s sysObjIDtxt %s sysServices %u ifNumber %u up since %s DB updated %s ", $deviceID , $SNMP->{sysName} , $SNMP->{sysDescr} , $SNMP->{sysContact} , $SNMP->{sysLocation}, $SNMP->{sysObjectID}, $SNMP->{sysObjIDtxt}, $SNMP->{sysServices}, $SNMP->{ifNumber} , YYYY_MM_DD_hh_mm_ss($SNMP->{UpSinceTime}), YYYY_MM_DD_hh_mm_ss($SNMP->{mtime}) ; INFO ("Updating\n$INFO\n") if $VERBOSE ; if ($SNMP->{DB}{sysObjectID}) # null value means we're creating a new device { # compare with existing info in DB my $devInfo = "Device ID: $deviceID name: " . ($SNMP->{sysName} ? $SNMP->{sysName} : "(no name) sysDescr: " . $SNMP->{sysDescr}) ; # has device been restarted since we last polled it? if (abs($SNMP->{UpSinceTime} - $SNMP->{DB}{UpSinceTime}) > 100 ) # allow dither in the time figures { Log_Event(sprintf "%s restarted: %s, previous restart: %s (dev: %u - DB: %u)", $devInfo, nameDate($SNMP->{UpSinceTime}), nameDate($SNMP->{DB}{UpSinceTime}), $SNMP->{UpSinceTime}, $SNMP->{DB}{UpSinceTime} ) ; } my @EventLog = (); unless ($SNMP->{DB}{sysName} eq $SNMP->{sysName} ) # device has changed name { push (@EventLog, sprintf "Device ID: %s sysName changed from: %s to: %s", $deviceID, $SNMP->{DB}{sysName}, $SNMP->{sysName} ) ; } unless ($SNMP->{DB}{sysContact} eq $SNMP->{sysContact} ) # sysContact changed { push (@EventLog, sprintf "%s sysContact changed from: %s to: %s", $devInfo, $SNMP->{DB}{sysContact}, $SNMP->{sysContact} ) ; } unless ($SNMP->{DB}{sysLocation} eq $SNMP->{sysLocation} ) # sysLocation changed { push (@EventLog, sprintf "%s sysLocation changed from: %s to: %s", $devInfo, $SNMP->{DB}{sysLocation}, $SNMP->{sysLocation} ) ; } Log_Event(@EventLog) if scalar @EventLog ; unless ($SNMP->{DB}{sysDescr} eq $SNMP->{sysDescr} ) # sysDescr changed { Log_Event (sprintf "%s sysDescr changed from: %s to: %s", $devInfo, $SNMP->{DB}{sysDescr}, $SNMP->{sysDescr} ) ; } unless ($SNMP->{DB}{sysServices} == $SNMP->{sysServices} ) # sysServices changed { Log_Event (sprintf "%s sysServices changed from: %u to: %u", $devInfo, $SNMP->{DB}{sysServices}, $SNMP->{sysServices} ) ; } unless ($SNMP->{DB}{ifNumber} == $SNMP->{ifNumber} ) # ifNumber changed { Log_Event (sprintf "%s number of interfaces changed from: %u to: %u", $devInfo, $SNMP->{DB}{ifNumber}, $SNMP->{ifNumber} ) ; } } else { %EVENT_INFO = ( Type => 'NEW_SNMP_DEVICE', deviceID => $deviceID, text => $INFO ) ; } my $DO = sprintf "REPLACE SNMP_system (deviceID,sysName,sysDescr,sysContact,sysLocation,sysObjectID,sysObjIDtxt,sysServices,ifNumber,UpSinceTime,mtime) VALUES ( %u, %s, %s, %s, %s, '%s', '%s', %u, %u, %u, %u)", $deviceID, $dbh->quote($SNMP->{sysName}) , $dbh->quote($SNMP->{sysDescr}) , $dbh->quote($SNMP->{sysContact}), $dbh->quote($SNMP->{sysLocation}), $SNMP->{sysObjectID} , $SNMP->{sysObjIDtxt} , $SNMP->{sysServices} , $SNMP->{ifNumber} , $SNMP->{UpSinceTime} , $SNMP->{mtime} ; $dbh->do($DO) ; # update ifTable ################### # delete previous records for this device $DO = sprintf "DELETE FROM SNMP_ifTable WHERE deviceID = %s", $deviceID ; $dbh->do($DO) ; my $INFO_format = "%5s %-20s %-16s %-20s %10s %-12s %-4s %-4s %s\n" ; $INFO = sprintf "\n$INFO_format" , 'Index', 'Descr', 'Name', 'Type', 'Speed', 'PhysAddress', 'Admn', 'Oper', 'last changed at' ; my $index; $dbh->do("DELETE from SNMP_ifTable where deviceID=$deviceID"); foreach $index (sort {$a<=>$b} keys %{$SNMP->{ifTable}}) { $INFO .= sprintf ($INFO_format, $index , ($SNMP->{ifTable}{$index}{ifDescr} or ''), ($SNMP->{ifTable}{$index}{ifName} or ''), $SNMP->{ifTable}{$index}{ifType}, ($SNMP->{ifTable}{$index}{ifSpeed} or 0), ($SNMP->{ifTable}{$index}{ifPhysAddress} or ''), $SNMP->{ifTable}{$index}{ifAdminStatus} , $SNMP->{ifTable}{$index}{ifOperStatus} , YYYY_MM_DD_hh_mm_ss($SNMP->{ifTable}{$index}{ifLastChangeAt})) ; $dbh->do(sprintf "INSERT SNMP_ifTable (deviceID,ifIndex,Descr,Name,Type,Speed,PhysAddress,AdminStatus,OperStatus,LastChangeAt) VALUES (%s,%s,%s,%s,%s,%u,%s,%s,%s,%u)", $deviceID , $index , $dbh->quote($SNMP->{ifTable}{$index}{ifDescr} or ''), $dbh->quote($SNMP->{ifTable}{$index}{ifName} or ''), $dbh->quote($SNMP->{ifTable}{$index}{ifType}), ($SNMP->{ifTable}{$index}{ifSpeed} or 0), $dbh->quote($SNMP->{ifTable}{$index}{ifPhysAddress} or ''), $dbh->quote($SNMP->{ifTable}{$index}{ifAdminStatus}) , $dbh->quote($SNMP->{ifTable}{$index}{ifOperStatus}) , $SNMP->{ifTable}{$index}{ifLastChangeAt} ) ; } INFO ($INFO) if $VERBOSE; unless ($SNMP->{DB}{sysObjectID}) # new device { $EVENT_INFO{text} .= $INFO; } # update IPaddrTable ###################### # delete previous records for this device $DO = sprintf "DELETE FROM SNMP_ipAddrTable WHERE deviceID = %s", $deviceID ; $dbh->do($DO) ; $INFO_format = "\t%5s %-15s %-15s\n" ; $INFO = sprintf "\n$INFO_format" , 'Index', 'IPaddress', 'NetMask' ; my $addr ; my $sth = $dbh->prepare("REPLACE SNMP_ipAddrTable (deviceID,IPadd,ifIndex,NetMask) VALUES (?,?,?,?)") ; foreach $addr (keys %{$SNMP->{ipAddrTable}}) { $INFO .= sprintf ($INFO_format, ($SNMP->{ipAddrTable}{$addr}{ipAdEntIfIndex} or '0'), $addr, $SNMP->{ipAddrTable}{$addr}{ipAdEntNetMask}) ; $sth->execute ( $deviceID , $addr, ($SNMP->{ipAddrTable}{$addr}{ipAdEntIfIndex} or 0), # dunno why, # was getting warning about use of uninitialised value here for some device (227.244) # although value (seen with snmpwalk etc) looked like 0 anyway $SNMP->{ipAddrTable}{$addr}{ipAdEntNetMask} ) ; } INFO ($INFO) if $VERBOSE ; unless ($SNMP->{DB}{sysObjectID}) # new device { $EVENT_INFO{text} .= $INFO; } if (%EVENT_INFO) # events to log? { Log_Event (sprintf "Device ID: %s %s\n%s\n", $EVENT_INFO{deviceID}, $EVENT_INFO{Type}, $EVENT_INFO{text}) ; } return $SNMP ; } ################################################################################################### sub get_SNMP_sysServices { # my $sysServices=shift or return 0; my $sysServices=shift or die "get_SNMP_sysServices called without value of sysServices to match"; my $Trecent = NOW() - $DAY ; my $SELECT = "SELECT deviceID, mtime FROM SNMP_system WHERE sysServices=$sysServices AND mtime>$Trecent ORDER BY mtime DESC"; # most recent first my $sth = $dbh->prepare ($SELECT) ; $sth->execute; my @devs = () ; if ($sth->rows) { my $devID; $sth->bind_col(1,\$devID); while ($sth->fetch) { push @devs, $devID ; } } else { WARNING "No devices with sysServices $sysServices found in SNMP_system database\n"; } $sth->finish ; return @devs; } ############################################################################## sub get_device_snmp # given structure containing IPadd, poll device for SNMP info and add it to hash { # my $SNMP = shift or return 0; my $SNMP = shift or die "get_device_snmp called without SNMP hashref" ; return 0 unless $SNMP->{IPadd} ; # $SNMP->{sess} = new SNMP::Session ( DestHost => $SNMP->{IPadd}); return 0 unless (get_device_snmp_system($SNMP)) ; # *** should test return values below: ?? get_device_snmp_interfaces($SNMP) ; get_device_snmp_ipAdEnt($SNMP) ; return 1 ; } ############################################################################## sub get_device_snmp_system # given SNMP structure containing session handle, return hash of system group info { # my $SNMP = shift or return 0 ; my $SNMP = shift or die "get_device_snmp_system called without SNMP hashref"; unless ($SNMP->{sess} ) # have we got an SNMP session handle? { return 0 unless $SNMP->{IPadd} ; # must have an IPadd $SNMP->{sess} = new SNMP::Session ( DestHost => $SNMP->{IPadd}); # create session } my $vars = new SNMP::VarList(['sysDescr',0], ['sysContact',0], ['sysLocation',0], ['sysName',0], ['sysObjectID',0], ['sysUpTime',0], ['sysServices',0],['ifNumber',0]); ($SNMP->{sysDescr}, $SNMP->{sysContact}, $SNMP->{sysLocation}, $SNMP->{sysName}, $SNMP->{sysObjectID}, $SNMP->{UpSinceTime}, $SNMP->{sysServices}, $SNMP->{ifNumber}) = $SNMP->{sess}->get($vars); return 0 if ($SNMP->{sess}->{ErrorStr}) ; # check that we got at least the mandatory fields: unless ($SNMP->{sysDescr}) { WARNING ("did not get sysDescr from device: name [%s] location [%s] objectID [%s] services [%s] contact [%s]\n", $SNMP->{sysName}, $SNMP->{sysLocation}, $SNMP->{sysObjectID}, $SNMP->{sysServices}, $SNMP->{sysContact}); return 0 } unless ($SNMP->{UpSinceTime}) { WARNING ("did not get sysUpTime from device: name [%s] descr [%s] location [%s] objectID [%s] services [%s] contact [%s]\n", $SNMP->{sysName}, $SNMP->{sysDescr}, $SNMP->{sysLocation}, $SNMP->{sysObjectID}, $SNMP->{sysServices}, $SNMP->{sysContact}); return 0 } unless ($SNMP->{sysObjectID}) { WARNING ("did not get sysObjectID from device: name [%s] descr [%s] location [%s] services [%s] contact [%s]\n", $SNMP->{sysName}, $SNMP->{sysDescr}, $SNMP->{sysLocation}, $SNMP->{sysServices}, $SNMP->{sysContact}); return 0 } unless ($SNMP->{sysServices}) { WARNING ("did not get sysServices from device: name [%s] descr [%s] location [%s] objectID [%s] contact [%s]\n", $SNMP->{sysName}, $SNMP->{sysDescr}, $SNMP->{sysLocation}, $SNMP->{sysObjectID}, $SNMP->{sysContact}); return 0 } $SNMP->{sysDescr} =~ s/\r//g; # get rid of CR chars in e.g. Cisco sysDescr $SNMP->{mtime} = time() ; $SNMP->{UpSinceTime} = int($SNMP->{mtime} - $SNMP->{UpSinceTime}/100) ; $SNMP->{sysObjIDtxt} = ''; # fudge! txt version should be textual translation of dotted version where available return 1; } ############################################################################## sub get_device_snmp_interfaces # given SNMP structure, get interfaces table info { # my $SNMP = shift or return 0 ; my $SNMP = shift or die "get_device_snmp_interfaces called without SNMP hashref"; return $SNMP if $SNMP->{ifTable} ; # return if we already have our info # check whether we already have system info and get it if necessary unless ($SNMP->{mtime}) { return 0 unless get_device_snmp_system($SNMP) ; } return 0 unless $SNMP->{sess} ; # have we got an SNMP session handle my $vars = new SNMP::VarList(['ifIndex'], ['ifDescr'], ['ifType'], ['ifSpeed'], ['ifPhysAddress'], ['ifAdminStatus'], ['ifOperStatus'], ['ifLastChange']); LOOP: { my ($Index, $Descr, $Type, $Speed, $PhysAddress, $AdminStatus, $OperStatus, $LastChange) = $SNMP->{sess}->getnext($vars) ; last LOOP unless $vars->[0]->tag eq 'ifIndex' ; # still in table? if ($SNMP->{sess}->{ErrorStr}) # not end of mib or other error { WARNING "got error $SNMP->{sess}->{ErrorStr} getting interfaces table from device $SNMP->{IPadd} $SNMP->{sysName}\n"; last LOOP ; } $SNMP->{ifTable}{$Index}{ifDescr} = $Descr ; $SNMP->{ifTable}{$Index}{ifName} = substr($Descr,0,20) ; # in case we don't get anything from ifName, below $SNMP->{ifTable}{$Index}{ifType} = $Type ; $SNMP->{ifTable}{$Index}{ifSpeed} = $Speed ; $SNMP->{ifTable}{$Index}{ifPhysAddress} = unpack "H*", $PhysAddress ; $SNMP->{ifTable}{$Index}{ifAdminStatus} = ($AdminStatus == 1) ? 'up' : 'down' ; $SNMP->{ifTable}{$Index}{ifOperStatus} = ($OperStatus == 1) ? 'up' : 'down' ; # ifLastChange "The value of sysUpTime at the time the interface entered its current # operational state. If the current state was entered prior to the last re-initialization # of the local network management subsystem, then this object contains a zero value." $SNMP->{ifTable}{$Index}{ifLastChangeAt} = int($SNMP->{UpSinceTime} + $LastChange/100) ; redo LOOP ; } # get ifNames: my ($Vendor, $Device) = ('',''); if (defined $SNMP->{sysObjectID} and defined $Device_Type{$SNMP->{sysObjectID}}) { ($Vendor, $Device) = @{$Device_Type{$SNMP->{sysObjectID}}} ; } return 1 if ($Vendor eq 'HP' and $Device eq 'J3245A Switch 800T') ; # for HP 800T skip ifName: ifDescr is more use # *** this wired-in stuff should be made a configuration variable, and extended to allow for the # *** same behaviour with other devices - could read a hash e.g. %skip_device_names {vendor} {device} # *************************************************************************************************** my @ifx = sort {$a <=> $b} keys %{$SNMP->{ifTable}} ; my @iflist ; foreach my $ix (@ifx) # each ifIndex { push @iflist, $ix; if (scalar @iflist > $MAX_SNMP_GET_VARS) # no more vars: get this lot { get_ifNames($SNMP, @iflist) ; @iflist = () ; # empty the list ... } } get_ifNames($SNMP, @iflist) ; return 1; } ############################################################################## sub get_ifNames # given SNMP structure and varlist, get listed ifNames and put in the ifNames structure { my ($SNMP, @iflist) = @_; # return unless scalar @iflist ; scalar @iflist or die "get_ifNames called without list of interfaces"; my @varlist ; foreach my $ix (@iflist) # build varlist with interface numbers { push @varlist, ['ifName', $ix ] ; } my $vars = new SNMP::VarList(@varlist) ; my @Name = $SNMP->{sess}->get($vars) ; if ($SNMP->{sess}->{ErrorStr}) # end of mib or other error { WARNING ("got error $SNMP->{sess}->{ErrorStr} getting %u ifNames from device $SNMP->{IPadd} $SNMP->{sysName}\n", scalar(@varlist)) unless ($SNMP->{sess}->{ErrorStr} =~ /noSuchName/) ; return ; } # (else) foreach my $ix (@iflist) # each interface number { my $Name = shift @Name ; $Name =~ s|^Slot (\d), Port (\d+)|$1/$2|; # prettify Accelar names $SNMP->{ifTable}{$ix}{ifName} = $Name ; } } ############################################################################## sub get_device_snmp_ipAdEnt # given SNMP structure, get ipAdEnt table info { # my $SNMP = shift or return 0 ; my $SNMP = shift or die "get_device_snmp_ipAdEnt called without SNMP hashref" ; unless ($SNMP->{mtime}) { return 0 unless get_device_snmp_system($SNMP) ; # get system info unless we already have it } return 0 unless $SNMP->{sess} ; # have we got an SNMP session handle my $vars = new SNMP::VarList(['ipAdEntAddr'], ['ipAdEntIfIndex'], ['ipAdEntNetMask']); LOOP: { my ($Addr, $Index, $Mask) = $SNMP->{sess}->getnext($vars) ; last LOOP unless $vars->[0]->tag eq 'ipAdEntAddr' ; # still in table? if ($SNMP->{sess}->{ErrorStr}) # not end of mib or other error { WARNING "got error $SNMP->{sess}->{ErrorStr} getting ipAddrTable from device $SNMP->{IPadd} $SNMP->{sysName}\n"; last LOOP ; } unless ($Index =~ /^\d+$/) # all digits { WARNING "got non-numeric IfIndex (ipAdEntIfIndex) from device $SNMP->{IPadd} $SNMP->{sysName}: Addr [$Addr] Index [$Index] Mask [$Mask]\n"; last LOOP ; } $SNMP->{ipAddrTable}{$Addr}{ipAdEntIfIndex}=$Index ; $SNMP->{ipAddrTable}{$Addr}{ipAdEntNetMask}=$Mask ; redo LOOP ; } return 1; } ############################################################################## sub device_access_IP # return an IP address by which device may be accessed { # my $devID = shift or return (); my $devID = shift or die "device_access_IP called without deviceID"; my $IPadd = '' ; foreach $IPadd (device_IPadds($devID)) # get addresses of device { next if $IPadd =~ /^127\.0\.0/ ; # skip loopback next if $IPadd =~ /^0\.0\.0/ ; # skip null return $IPadd if ping ($IPadd); # OK if dev is pingable } return ''; # quit unless we find a live interface } ############################################################################## sub device_IPadds # returns list of IP addresses of device { # my $devID = shift or return (); my $devID = shift or die "device_IPadds called without deviceID"; my ($IPadd, @IPadds); my $sth = $dbh->prepare("SELECT IPadd FROM SNMP_ipAddrTable WHERE deviceID=$devID") ; $sth->execute ; return () unless ($sth->rows) ; # found any? $sth->bind_col(1, \($IPadd) ); while ($sth->fetch) { push @IPadds, $IPadd ; } return @IPadds ; } ############################################################################## sub names_by_IPadd { # my $IPadd = shift or return (); my $IPadd = shift or die "names_by_IPadd called without IPadd"; my ($name, @names); my $sth = $dbh->prepare("SELECT name FROM IP_hosts WHERE IPadd='$IPadd'") ; $sth->execute ; return () unless ($sth->rows) ; # found any? $sth->bind_col(1, \($name) ); while ($sth->fetch) { push @names, $name ; } return @names ; } ################################################################################################### sub update_device_dot1d_table # given ref to SNMP info hash, or deviceID, or device's IP address; # get the dot1d table (checking first that we have got the interfaces table) # and update the dot1d DB table # RETURN: nothing { # my $device = shift or return ; my $device = shift or die "update_device_dot1d_table called without device ID, ref to SNMP info or IPadd"; # value may be IP address or deviceID my (%SNMP, $deviceID) ; if (ref($device) eq 'HASH') # passed ref to SNMP hash { %SNMP = %{$device} ; $deviceID = $SNMP{DB}{deviceID}; $VERBOSE and INFORM "update_device_dot1d_table(HASH: deviceID:$deviceID)\n"; } elsif ($device =~ /^\d+$/) # deviceID, not IPadd { INFORM "update_device_dot1d_table(deviceID: $device)\n"; my $IPadd = device_access_IP($device) ; return unless $IPadd; # quit unless we got a live interface $SNMP{IPadd} = $IPadd ; $SNMP{DB}{deviceID} = $device ; } else # get deviceID for this IPadd { INFORM "update_device_dot1d_table(IPadd: $device)\n"; unless ($deviceID = deviceID_by_IPadd($device)) { WARNING "Error could not get deviceID for $device\n"; return ; } $SNMP{DB}{deviceID} = $deviceID ; $SNMP{IPadd} = $device ; } return unless get_device_snmp_interfaces(\%SNMP) ; &SNMP::loadModules('BRIDGE-MIB') ; my $vars = new SNMP::VarList(['dot1dTpFdbPort'], ['dot1dTpFdbAddress'], ['dot1dTpFdbStatus']); my (%port_addr, %port_nMACs) ; LOOP: { my ($Port, $Addr, $Status) = $SNMP{sess}->getnext($vars) ; last LOOP unless $vars->[0]->tag eq 'dot1dTpFdbPort' ; # still in table? if ($SNMP{sess}->{ErrorStr}) # not end of mib or other error { WARNING "got error $SNMP{sess}->{ErrorStr} getting dot1dTpFdb* from device $SNMP{IPadd} $SNMP{sysName}\n"; last LOOP ; } redo LOOP if $Status == 4 ; # skip self(4) records, only interested in learned(3) and others $Addr = unpack "H*", $Addr ; $port_addr{$Port}{$Addr} = time() ; $port_nMACs{$Port}++; redo LOOP ; } foreach my $ifIndex (keys %port_addr) { foreach my $MAC (keys %{$port_addr{$ifIndex}}) { $dbh->do("REPLACE dot1d VALUES ($deviceID, $ifIndex, '$MAC', $port_nMACs{$ifIndex}, $port_addr{$ifIndex}{$MAC})") ; } } } ############################################################################## sub update_device_arp_table # given deviceID or IPaddress, retrieve arp (ipNetToMedia) table from device # and update DB arp and dot1d tables { #my $device = shift or return ; # device name or address passed as argument my $device = shift or die "update_device_arp_table called without device name or IP address"; # value may be IP address or deviceID INFORM "get_update_arp_table($device)\n"; my $IPadd = $device ; if ($device =~ /^\d+$/) # deviceID, not IPadd { $IPadd = device_access_IP($device) ; return 0 unless $IPadd; # quit unless we got a live interface } else # get deviceID for this IPadd { $device = deviceID_by_IPadd($IPadd); return 0 unless $device ; } my %SNMP ; $SNMP{IPadd} = $IPadd ; $SNMP{DB}{deviceID} = $device ; return 0 unless get_device_snmp_system(\%SNMP) ; return 0 unless $SNMP{sysObjectID} ; # check we have a mandatory field my $ACCELAR = 0 ; if ($Device_Type{$SNMP{sysObjectID}} and ($Device_Type{$SNMP{sysObjectID}}->[1] =~ /^Accelar/)) { $ACCELAR = 1 ; } my $vars = new SNMP::VarList(['ipNetToMediaIfIndex'], ['ipNetToMediaPhysAddress'], ['ipNetToMediaNetAddress'], ['ipNetToMediaType']); my $records = 0 ; my (%port_addr, %port_nMACs) ; # data structures used for updating dot1d table LOOP: { my ($Index, $Phys, $Net, $Type) = $SNMP{sess}->getnext($vars) ; my $mtime=time(); last LOOP unless $vars->[0]->tag eq 'ipNetToMediaIfIndex' ; # still in table? if ($SNMP{sess}->{ErrorStr}) # not end of mib or other error { WARNING "got error $SNMP{sess}->{ErrorStr} getting ipNetToMediaTable from device $SNMP{IPadd} $SNMP{sysName}\n"; last LOOP ; } redo LOOP unless $Type > 2 ; # skip 'other'(1) entries (local & broadcast addresses), only interested in dynamic(3) and static(4) $Phys = unpack "H*", $Phys ; creup_ARP_record($mtime, $Net, $Phys); # update DB if ($ACCELAR) { $Index = $Index>>16 ; # shift to get ifIndex associated with this IP+MAC address } $port_addr{$Index}{$Phys} = $mtime ; $port_nMACs{$Index}++; # update_dot1d_record($device, time(), $Index, $Phys) ; $records++ ; # count records we get redo LOOP ; } foreach my $ifIndex (keys %port_addr) { foreach my $MAC (keys %{$port_addr{$ifIndex}}) { $dbh->do("REPLACE dot1d VALUES ($device, $ifIndex, '$MAC', $port_nMACs{$ifIndex}, $port_addr{$ifIndex}{$MAC})") ; } } return $records ; } ############################################################################## sub creup_ARP_record # update ARP record in DB { my ($time2, $IP, $MAC) = @_ ; $MAC or die "creup_ARP_record called without time, IP and MAC"; my $time1 = $time2; my $FUNCNAME = 'creup_ARP_record' ; # retrieve current record for this IP address; # where multiple records exist (e.g. duplicate IP addresses) # get the most recent by sorting on latest time in reverse order (DESC) # and taking only the first record returned: my $sth = $dbh->prepare("SELECT MAC, time1, time2 FROM IP_MAC WHERE IPadd='$IP' ORDER BY time2 DESC") ; $sth->execute ; my $matches = $sth->rows ; my ($db_MAC, $db_time1, $db_time2) ; if ($matches) { # Bind variable to columns: $sth->bind_columns(undef, \($db_MAC, $db_time1, $db_time2)); $sth->fetch ; if ($MAC eq $db_MAC) # is MAC address seen now same as in db? { if ($time2 < $db_time2) # earlier than latest in db? (not normal - maybe importing old data) { INFORM "$FUNCNAME: updating $time2 earlier than db record time2 $db_time2\n"; $time2 = $db_time2 ; # keep more recently updated time in db } if ($time1 >= $db_time1) # normal case { $time1 = $db_time1 ; # keep older time1 from db } else { INFORM "$FUNCNAME: updating $time1 earlier than db record time1 $db_time1\n"; } # update only the selected record! $dbh->do("UPDATE IP_MAC SET time1=$time1, time2=$time2 WHERE IPadd='$IP' AND MAC='$db_MAC' AND time1=$db_time1 and time2=$db_time2") ; return 1; # OK, done } # (else) different MAC address Log_Event (sprintf "Changed MAC or duplicate IP address: %-15s %s -> %s", $IP, $db_MAC, $MAC); } # (else) INFO "+arp\t%-15s %s %s %s\n", $IP, $MAC, YYYY_MM_DD_hh_mm_ss($time1), YYYY_MM_DD_hh_mm_ss($time2) if $VERBOSE ; my $DO = "INSERT INTO IP_MAC VALUES ('$IP', '$MAC', $time1, $time2)" ; $dbh->do($DO) ; Log_Event (sprintf "New device: %-15s %s found", $IP, $MAC) unless $matches; # no 'new' log message for changed MAC address } ################################################################################################### __END__