# collect.pl # # Collect IP accounting data from a cisco router and summarise it # into a CSV file. The file shows number of bytes sent from each # source device to each destination network. # Optionally, the data is also summarised by source (all destinations added # together) and written out to an mrtg config file. # There are also two input files. "networks" is used to map network # addresses to names. "sources" is optional & is used to map source # addresses to host names. # # Args are: # h help # a path of mrtg config file for data by source # n path of a "networks" file used to map network addresses to names # o path of output CSV file. Defaults to YYYYMMDD-HHMMSS.CSV # s path of a "sources" file mapping source addresses to host names # # v1.0 17/9/98 Tony Farr Original # v1.1 16/12/98 Tony Farr Add horrid kludge to collect Exchange traffic # by source # v1.2 22/3/99 Tony Farr Tidy up # v1.3 25/3/99 Tony Farr Remove v1.1 kludge # Just generate one mrtg line/data set # use SNMP_util; use Getopt::Std; use File::Basename; use strict; use Socket; use vars qw/$opt_h $opt_a $opt_n $opt_o $opt_s/; # CONSTANTS # Note inclusion of the write community string my $HOST= '0ztrad3@canb-wan'; # Directory for output logs/csv files. my $LOGPATH= "D:\\logs\\whodo\\"; # Directory for MRTG config files for traffic sources my $SOURCEDIR= "D:\\www\\mrtg\\whodo"; # Any source generating more than BIGBYTES per poll will be added to the sources config file automatically my $BIGBYTES= 40000000; my(@dstdesc, @dstaddr, @dstmask, @srcaddr, @srcdesc, %traffictab); my $progname = basename($0); my $usage = "Usage: $progname [-h] [-a mrtg_config_file] [-n network_file] [-o output_file] [-s source_address_file]\n"; # Parse Command Line: die $usage unless getopts('ha:n:o:s:'); if ( defined($opt_h) ) { print $usage; exit(0); } if ( defined($opt_n) ) { load_nets($opt_n); } unless ( defined($opt_o) ) { my ($sec,$min,$hour,$mday,$month,$year) = localtime; $opt_o= $LOGPATH . sprintf("%d%02d%02d-%02d%02d%02d.csv", $year+1900,++$month,$mday,$hour,$min,$sec); } if ( defined($opt_s) ) { load_sources($opt_s); } my $age= checkpoint_stats($HOST); get_stats($HOST); print_stats($opt_o); if ( defined($opt_a) ) { make_sources_config($opt_a); } exit(0); sub load_nets { # Loads 3 arrays: # @dstaddr - a list of IP addresses # @dstdesc - a list of the corresponding descriptions # @dstmask - a list of the corresponding bitmasks # The tables are loaded from a list of filenames passed in @_. That file # can be a networks file. The file(s) should have format: # netdescription 10.10.10 /25 # The "/25" is the mask & may be preceeded by a "#". It is optional. my(@flist)= @_; my ($desc, $end, $masksz); while (my $fname= shift(@flist)) { open(NETWORKS, "<$fname") || warn "$progname: unable to open $fname; $!"; while (<NETWORKS>) { # Process #includes after dealing with the current file if ( /^#\s*include\s+(\S+)/ ) { push(@flist, $1); next; } if ( /^\s*(\w+)\s+(\S+)(.*)/ ) { $desc= $1; $_= $2; $end= $3; push(@dstdesc, $desc); my $octets = 1 + tr/././; $_ .= ".0" x (4 - $octets); push( @dstaddr, aton($_) ); if ( $end =~ /\/(\d+)/ ) { $masksz= $1; } else { $masksz= $octets * 8; } push( @dstmask, pack("B32", "1" x $masksz . "0" x 32) ); } } close(NETWORKS); } } sub load_sources { # Loads a pair of arrays: @srcaddr (a list of IP addresses) and @srcdesc # (a list of the corresponding descriptions). The files are loaded from # a list of filenames passed in @_. That file can be a hosts file. However "address" entries # can be perl regular exprs as well literal addresses. For compatibility, # "." when used as a wild card must be expressed "\." - i.e. the reverse of normal. my(@flist)= @_; while (my $fname= shift(@flist)) { open(HOSTS, "<$fname") || warn "$progname: unable to open $fname; $!"; while (<HOSTS>) { # Process #includes after dealing with the current file if ( /^#\s*include\s+(\S+)/ ) { push(@flist, $1); next; } ($_)= split(/#/); if ( /(\S+)\s+(\S+)/ ) { push(@srcdesc, $2); $_= $1; s/\./\\\./g; # Replace "." with "\." s/\\\\\././g; # If we now have "\\.", replace with "." push(@srcaddr, $_); } } close(HOSTS); } } sub checkpoint_stats { # Take a checkpoint on IP accounting on the given router & return the duration of it. # The checkpoint is done by doing a get then a set on actCheckPoint my ($age); # Find how long since the last checkpoint ($age) = snmpget ($_[0], '1.3.6.1.4.1.9.2.4.8.0'); warn "$progname: No actAge returned.\n" unless $age; # Check to see if we've lost any data ($_) = snmpget ($_[0], '1.3.6.1.4.1.9.2.4.6.0'); warn "$progname: Accounting table overflow - $_ bytes lost.\n" if $_ > 0; # Do a new checkpoint ($_) = snmpget ($_[0], '1.3.6.1.4.1.9.2.4.11.0'); die "$progname: No actCheckPoint returned.\n" unless defined; snmpset ($_[0], '1.3.6.1.4.1.9.2.4.11.0', 'integer', $_); $age; } sub get_stats { # Summarise the checkpoint by destination network (not host). # Summary is placed into %traffictab - a hash of hashes indexed by # source device & destination network. my($src, $dstnet); my @response = snmpwalk ($_[0], '1.3.6.1.4.1.9.2.4.9.1.4' ); foreach $_ (@response) { /(\d+\.\d+\.\d+\.\d+)\.(\d+\.\d+\.\d+\.\d+):(\d+)/ || die "$progname: Cannot parse response from walk.\n"; $dstnet= addr_to_net($2); $src= addr_to_src($1); $traffictab{$src}{$dstnet} += $3; } } sub print_stats { # Print out the traffictab in csv format my ($sec,$min,$hour,$mday,$month,$year) = localtime(time()); $year += 1900; $month++; open (CSVFILE,">$_[0]") || die "$progname: Could not open file $_[0]; $!\n"; printf CSVFILE "End Time:,%d/%02d/%d %d:%02d:%02d\n",$mday,$month,$year,$hour,$min,$sec; print CSVFILE "Duration:,$age\n"; # Bug alert. This breaks if $age > 1 day my($s, $d); foreach $s (sort keys %traffictab) { foreach $d (sort keys %{$traffictab{$s}}) { print CSVFILE "$s,$d,$traffictab{$s}{$d}\n"; } } close(CSVFILE); } sub make_sources_config { # Print out an mrtg config file my($cfgfile)= @_; my(%cfgentries, $src, $dst, $t, $misc); # Load current cfg entries if ( open(CFG, "<$cfgfile") ) { while (<CFG>) { if ( /^\s*Target\[([^\]]*)/ && $1 ne "Miscellaneous" ) { $cfgentries{ uc($1) }= 1; } } close(CFG); } # Write out the header of a new config file open(CFG,">$cfgfile") || die "$progname: Could not open file $cfgfile; $!\n"; write_sources_header(); # For each traffictab entry, if it's large or there's an existing CFG entry, write out a new CFG entry foreach $src (keys %traffictab) { $t= 0; foreach $dst (keys %{$traffictab{$src}}) { $t += $traffictab{$src}{$dst}; } if ( $cfgentries{ uc($src) } ) { delete $cfgentries{ uc($src) }; write_source_entry($src, $t); } elsif ( $t > $BIGBYTES ) { write_source_entry($src, $t); } else { $misc += $t; } } # Write out new entries for any CFG entries that existed previously but we've # missed because they generated no traffic this time. foreach $src (keys %cfgentries) { write_source_entry($src, 0); } # Write an entry for the miscellaneous odds & ends write_source_entry("Miscellaneous", $misc); close(CFG); } sub write_sources_header { print CFG <<END_OF_HEADER; WorkDir: $SOURCEDIR IconDir: /mrtg/ Interval: 30 END_OF_HEADER } sub write_source_entry { print CFG <<END_OF_ENTRY; Title[$_[0]]: Traffic from $_[0] PageTop[$_[0]]: <H1>Traffic from $_[0]</H1> MaxBytes[$_[0]]: 12500000 Options[$_[0]]: growright, bits, absolute, nopercent Colours[$_[0]]: w#ffffff,blue#0000e0,w#ffffff,r#ff0000 Target[$_[0]]: `perl -e "print \\"0\\n$_[1]\\""` YLegend[$_[0]]: Bits per Second ShortLegend[$_[0]]: bps Legend1[$_[0]]: Legend2[$_[0]]: Traffic from $_[0] LegendI[$_[0]]: LegendO[$_[0]]: Traffic: END_OF_ENTRY } sub addr_to_net { # Returns the name/description of the network of the given address. # Addresses are looked up in @dstaddr first. If that fails, the address is returned. my($i, $dst); $dst= aton($_[0]); for ($i=0; $i < @dstaddr; $i++) { if ( ($dst & $dstmask[$i]) eq $dstaddr[$i] ) { return $dstdesc[$i]; } } $_[0] =~ /(.*)\..*/; # Assume Class C & strip off the last octet $1; } sub BEGIN { my ($lastaddr, $lastsrc); sub addr_to_src { # Returns the name/description of the given address. # Addresses are looked up in @srcaddr first. If there's no match, a dns lookup is tried. # If that fails, the address is returned. if ( $_[0] eq $lastaddr ) { return $lastsrc; } else { $lastaddr= $_[0]; for (my $i=0; $i < @srcaddr; $i++) { if ($_[0] =~ /^$srcaddr[$i]$/ ) { $lastsrc= $srcdesc[$i]; return $lastsrc; } } my $addr= aton($_[0]); if ($lastsrc= gethostbyaddr($addr, AF_INET)) { $lastsrc =~ s/\.austrade\.gov\.au$//i; } else { $lastsrc= $_[0]; } return $lastsrc; } } } sub aton { # I found the standard "inet_aton" very slow (on Windows). # Hence this version. It only handles dotted decimal addresses - # not names. $_[0] =~ /(\d+).(\d+).(\d+).(\d+)/; chr($1).chr($2).chr($3).chr($4); }