#! /usr/bin/perl # physaddrwatch - a utility to monitor usage of media physical addresses by # IP and AppleTalk addresses using SNMP tables on routers # (ipNetToMediaPhysAddr, aarpPhysAddress) # Copyright (C) 1998-2005 Dave Plonka =head1 NAME physaddrwatch - monitor usage of media physical addresses =head1 SYNOPSIS usage: physaddrwatch [ -hvq ] [ C<-d> data_file ] [ C<-s> ds[,arg2,...]] [ C<-c> community ] gateway [ ... ] C<-h> - help C<-d> data_file - use this data file (defaults to "physaddrwatch.dat") C<-s> ds[,...] - use "physaddr" table in SQL database rather than a data file. (Or in addition to a data file if "-d data_file" is specified as well.) The specified value will be used as argument(s) to DBI->connect C<-v> - verbose C<-q> - quiet (surpresses non-fatal warnings) C<-c> - community (default: "public") =head1 DESCRIPTION B is a utility to monitor usage of media physical addresses by IP and AppleTalk addresses using SNMP tables on routers (B, B). This utility maintains a data file (e.g. "physaddrwatch.dat") containing three white-space seperated columns. These columns contain, in order, the IP or AppleTalk address, physical ethernet (MAC) or ATM address, and the date and time. B locks its data file while polling each gateway. This prevents the file from becoming corrupt by having multiple processes attempt to write it simultaneously. =head1 EXAMPLES physaddrwatch router physaddrwatch -d router.dat router mysql test <<_EOF_ CREATE TABLE physaddr ( physaddress VARCHAR(40) NOT NULL, address CHAR(16) NOT NULL, dt DATETIME NOT NULL, PRIMARY KEY (physaddress, address), KEY (physaddress), KEY (address) ); _EOF_ physaddrwatch -vs "DBI:mysql:test;mysql_read_default_file=`ls ~/.my.cnf`" router =head1 AUTHOR Dave Plonka =cut require "getopts.pl"; require 5.002; use Altoids 1.014; use Fcntl; use POSIX; # for ENOENT '$Revision: 1.9 $' =~ m/(\d+)\.(\d+)/ && (( $VERSION ) = sprintf("%d.%03d", $1, $2)); $script = $0; $script =~ s:^.*/::; $default_community = 'public'; $oidsdir = '/usr/local/lib/oids'; $datafile = "${script}.dat"; # default data file $tmpdata = ".${script}.dat"; # it's not necessary to put the process id in the ;# file name because we only create this while we ;# have a write/exclusive lock on the data file. @dsn = (); # e.g.: ('DBI:mysql:test;mysql_read_default_file=' . <~/.my.cnf>); $inserts = 0; $updates = 0; %port = (); if (!&Getopts("hd:Vvqc:s:") || $opt_h || 0 > $#ARGV) { print STDERR <<_EOF_ usage: $script [ -hvq ] [ -d data_file ] [-s ds[,arg2,...]] gateway [ ... ] -h - help -d data_file - use this data file (defaults to "${script}.dat") -s ds[,...] - use "physaddr" table in SQL database rather than a data file. (Or in addition to a data file if "-d data_file" is specified as well.) The specified value will be used as argument(s) to DBI->connect -v - verbose -q - quiet (surpresses non-fatal warnings) -c - community (default: \"$default_community\") _EOF_ ; exit 2 } if ($opt_s) { # update a table in a database using DBI... @dsn = split(m/,/,$opt_s); require 'DBI.pm' && import DBI } elsif ($opt_d) { $datafile = $opt_d } else { $opt_d = $datafile } $Altoids::verbose = $opt_V; $Altoids::quiet = $opt_q; ;############################################################################### ;# populate a translation base with info from any SunNet Manager(?) OID files ;# such as those produced by MIB2SCHEMA. (http://www.cisco.com/public/mibs/oid) push(@oidfiles, "${oidsdir}/RFC1213-MIB.oid", # for ipNetToMediaPhysAddress "${oidsdir}/RFC1243-MIB.oid", # for aarpPhysAddress ); print STDERR "Constructing Altoids..." if $opt_v; $alt = Altoids->new(@oidfiles); print STDERR " done.\n" if $opt_v; %entries = (); if ($opt_s) { $dbh = DBI->connect(@dsn) || die $dbh->errstr; $update = $dbh->prepare(q{ UPDATE physaddr SET dt = ? WHERE physaddress = ? AND address = ? }) || die($dbh->errstr); $insert = $dbh->prepare(q{ INSERT INTO physaddr (physaddress, address, dt) VALUES (?, ?, ?) }) || die($dbh->errstr); $insert->{PrintError} = 0; } if ($opt_d) { if (!open(DATA, "+<$datafile")) { die "open \"$datafile\" for update: $!\n" if (ENOENT != $!); open(DATA, ">$datafile") || die "open \"$datafile\": $!\n" } ;# lock the file for exclusive access $struct_flock = pack("sslll", F_WRLCK, SEEK_SET, 0, 0); if (!fcntl(DATA, F_SETLK, $struct_flock)) { if (EAGAIN == $? && $opt_q) { exit(0) } else { die "exclusive lock on \"$datafile\" failed: $!\n"; } } ;# load the entries from the data file while () { if (m/^\s*#/) { push(@comments, $_) } else { ($address, $physaddress, $times) = split; $entries{$address}{$physaddress} = $times; # if ($time_t =~ m;^(\d\d\d\d)/(\d\d)/(\d\d)_(\d\d):(\d\d):(\d\d)$;) { # $time_t = timelocal($6, $5, $4, $3, $2-1, $1-1900); # } } } } foreach $host (@ARGV) { $alt->open($host, $opt_c? $opt_c : $default_community, 161); $name = 'ipNetToMediaPhysAddress'; print STDERR "Walking $host - \"$name\"... " if $opt_v; my %retblock = $alt->walk($name); print STDERR "done.\n" if $opt_v; $date = strftime("%Y/%m/%d_%H:%M:%S", localtime); if ($retblock{-1} == -1) { warn "${host}: SNMP walk of ${name} failed\n"; goto close_label; } hashconvert('', %retblock); $name = 'aarpPhysAddress'; print STDERR "Walking $host - \"$name\"... " if $opt_v; my %retblock = $alt->walk($name); print STDERR "done.\n" if $opt_v; $date = strftime("%Y/%m/%d_%H:%M:%S", localtime); if ($retblock{-1} == -1) { warn "${host}: SNMP walk of ${name} failed\n"; goto close_label; } hashconvert('', %retblock); close_label: $alt->close(); } if ($opt_s) { print "$updates row(s) udpated, $inserts row(s) inserted\.\n" if $opt_v; $dbh->disconnect } if ($opt_d) { open(NEWDATA, ">$tmpdata") || die "open \"$tmpdata\": $!\n"; print(NEWDATA @comments) || die "print NEWDATA: $!\n"; hashwrite('', %entries); close(NEWDATA); rename($tmpdata, $datafile) || die "rename \"$tmpdata\" \"$datafile\": $!\n"; ;# unlock the "old" data file (now that we've clobbered it) $struct_flock = pack("sslll", F_UNLCK, 0, 0, 0); fcntl(DATA, F_SETLK, $struct_flock) || die "unlock failed: $!\n"; close(DATA); } exit; ;############################################################################### sub hashconvert { my $prefix = shift @_; my %hash = @_; my ($val, $key); while (($key, $val) = each(%hash)) { if (ref($val) eq 'HASH') { hashconvert($prefix? $prefix . '.' . $key : $key, %{$val}) } else { my $address; if ($prefix =~ m/^ipNetToMediaPhysAddress\.\d+\.(.*)$/) { $address = $1 . '.' . $key } elsif ($prefix =~ m/^aarpPhysAddress\.(\d+)\.(\d+)\.(\d+)$/) { $address = (256 * $2 + $3) . '.' . $key } if ($opt_s) { my $rv; if ($rv = $insert->execute($val, $address, $date)) { die "insert->execute returned $rv" if (1 != $rv); $inserts++ } else { # insert failed - assume it was a duplicate row $rv = $update->execute($date, $val, $address) || die $update->errstr; # update should affect no more than 1 row: die "update->execute returned $rv" if (1 < $rv); $updates++ } } if ($opt_d) { $entries{$address}{$val} = $date } } } } sub hashwrite { my $prefix = shift @_; my %hash = @_; my ($val, $key); while (($key, $val) = each(%hash)) { if (ref($val) eq 'HASH') { hashwrite($prefix? $prefix . '.' . $key : $key, %{$val}) } else { # format STDOUT_TOP = # IP/AppleTalk Physical Address (ethernet, atm) Whence last seen # Address # --------------- ------------------------------------------ ------------------- # . format NEWDATA = @<<<<<<<<<<<<<< @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< @<<<<<<<<<<<<<<<<<< $prefix $key $val . write NEWDATA } } }