From 10551637480512ea273847298f4e58295c64e37b Mon Sep 17 00:00:00 2001 From: Manuel Kasper Date: Thu, 22 Jan 2009 08:30:10 +0000 Subject: [PATCH] + AS stats script for sFlow --- bin/sflow-asstatd.pl | 272 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 272 insertions(+) create mode 100644 bin/sflow-asstatd.pl diff --git a/bin/sflow-asstatd.pl b/bin/sflow-asstatd.pl new file mode 100644 index 0000000..37d745d --- /dev/null +++ b/bin/sflow-asstatd.pl @@ -0,0 +1,272 @@ +#!/usr/bin/perl -w +# +# $Id$ +# +# written by Manuel Kasper, Monzoon Networks AG + +use strict; +use Net::sFlow; +use IO::Socket; +use RRDs; + +if ($#ARGV != 1) { + die("Usage: $0 \n"); +} + +my $rrdpath = $ARGV[0]; +my $knownlinksfile = $ARGV[1]; + +if (! -d $rrdpath) { + die("$rrdpath does not exist or is not a directory\n"); +} + +my %knownlinks; + +my $myas = 8302; +my $samplingrate = 512; + +my $ascache = {}; +my $ascache_lastflush = 0; +my $ascache_flush_interval = 60; + +my $server_port = 6343; +my $MAXREAD = 8192; +my $header_len = 28; +my $flowrec_len = 28; +my $childrunning = 0; + +# reap dead children +$SIG{CHLD} = \&REAPER; +$SIG{TERM} = \&TERM; +$SIG{INT} = \&TERM; + +sub REAPER { + wait; + $childrunning = 0; + $SIG{CHLD} = \&REAPER; +} + +sub TERM { + print "SIGTERM received\n"; + exit 0; +} + +# read known links file +read_knownlinks(); + +# prepare to listen for sFlow UDP packets +my $server = IO::Socket::INET->new(LocalPort => $server_port, Proto => "udp") + or die "Couldn't be a udp server on port $server_port : $@\n"; + +my ($him,$datagram,$flags); + +# main sFlow datagram receive loop +while (1) { + $him = $server->recv($datagram, $MAXREAD); + next if (!$him); + + my ($port, $ipaddr) = sockaddr_in($server->peername); + + # decode the sFlow packet + my ($sFlowDatagramRef, $sFlowSamplesRef, $errorsRef) = Net::sFlow::decode($datagram); + + if ($sFlowDatagramRef->{'sFlowVersion'} != 5) { + print "Warning: non-v5 packet received - not supported\n"; + next; + } + + foreach my $sFlowSample (@{$sFlowSamplesRef}) { + # only process standard structures + next if ($sFlowSample->{'sampleTypeEnterprise'} != 0); + + # only process normal flow samples + next if ($sFlowSample->{'sampleTypeFormat'} != 1); + + my $snmpin = $sFlowSample->{'inputInterface'}; + my $snmpout = $sFlowSample->{'outputInterface'}; + + if ($snmpin >= 1073741823 || $snmpout >= 1073741823) { + # invalid interface index - could be dropped packet or internal + # (routing protocol, management etc.) + #print "Invalid interface index $snmpin/$snmpout\n"; + next; + } + + my $noctets; + if ($sFlowSample->{'IPv4Packetlength'}) { + $noctets = $sFlowSample->{'IPv4Packetlength'}; + } else { + $noctets = $sFlowSample->{'HeaderFrameLength'} - 14; + } + + my $srcas = 0; + my $dstas = 0; + + if ($sFlowSample->{'GatewayAsSource'}) { + $srcas = $sFlowSample->{'GatewayAsSource'}; + } + if ($sFlowSample->{'GatewayDestAsPaths'}) { + $dstas = pop(@{$sFlowSample->{'GatewayDestAsPaths'}->[0]->{'AsPath'}}); + if (!$dstas) { + $dstas = 0; + } + } + + # Outbound packets have our AS number as the source (GatewayAsSource), + # while inbound packets have 0 as the destination (empty AsPath). + # Transit packets have "foreign" AS numbers for both source and + # destination (handleflow() currently deals with those by counting + # them twice; once for input and once for output) + + # substitute 0 for own AS number + if ($srcas == $myas) { + $srcas = 0; + } + if ($dstas == $myas) { + $dstas = 0; + } + + handleflow($ipaddr, $noctets, $srcas, $dstas, $snmpin, $snmpout); + } +} + +sub handleflow { + my ($routerip, $noctets, $srcas, $dstas, $snmpin, $snmpout) = @_; + + if ($srcas == 0 && $dstas == 0) { + # don't care about internal traffic + return; + } + + #print "$srcas => $dstas ($noctets octets)\n"; + + # determine direction and interface alias name (if known) + my $direction; + my $ifalias; + my $as; + + if ($srcas == 0) { + $as = $dstas; + $direction = "out"; + $ifalias = $knownlinks{inet_ntoa($routerip) . '_' . $snmpout}; + } elsif ($dstas == 0) { + $as = $srcas; + $direction = "in"; + $ifalias = $knownlinks{inet_ntoa($routerip) . '_' . $snmpin}; + } else { + handleflow($routerip, $noctets, $srcas, 0, $snmpin, $snmpout); + handleflow($routerip, $noctets, 0, $dstas, $snmpin, $snmpout); + return; + } + + if (!$ifalias) { + # ignore this, as it's through an interface we don't monitor + return; + } + + my $dsname = "${ifalias}_${direction}"; + + # put it into the cache + if (!$ascache->{$as}) { + $ascache->{$as} = {createts => time}; + } + + $ascache->{$as}->{$dsname} += $noctets; + $ascache->{$as}->{updatets} = time; + + if ($ascache->{$as}->{updatets} == $ascache_lastflush) { + # cheat a bit here + $ascache->{$as}->{updatets}++; + } + + # now flush the cache, if necessary + flush_cache(); +} + +sub flush_cache { + + if ($childrunning || ((time - $ascache_lastflush) < $ascache_flush_interval)) { + # can't/don't want to flush cache right now + return; + } + + my $pid = fork(); + + if (!defined $pid) { + print "cannot fork\n"; + } elsif ($pid != 0) { + # in parent + $childrunning = 1; + $ascache_lastflush = time; + $ascache = {}; + return; + } + + while (my ($as, $cacheent) = each(%$ascache)) { + print "$$: flushing data for AS $as ($cacheent->{updatets})\n"; + + my $rrdfile = getrrdfile($as, $cacheent->{updatets}); + my @templatearg; + my @args; + + while (my ($dsname, $value) = each(%$cacheent)) { + next if ($dsname !~ /_(in|out)$/); + + push(@templatearg, $dsname); + push(@args, $value * $samplingrate); + } + + RRDs::update($rrdfile, "--template", join(':', @templatearg), + $cacheent->{updatets} . ":" . join(':', @args)); + my $ERR = RRDs::error; + if ($ERR) { + print "Error updating RRD file $rrdfile: $ERR\n"; + } + } + + exit 0; +} + +# create an RRD file for the given AS, if it doesn't exist already, +# and return its file name +sub getrrdfile { + my $as = shift; + my $startts = shift; + $startts--; + + # let's see if there's already an RRD file for this AS - if not, create one + my $rrdfile = "$rrdpath/$as.rrd"; + if (! -r $rrdfile) { + #print "$$: creating RRD file for AS $as\n"; + + my @args; + while (my ($key, $alias) = each(%knownlinks)) { + push(@args, "DS:${alias}_in:ABSOLUTE:300:U:U"); + push(@args, "DS:${alias}_out:ABSOLUTE:300:U:U"); + } + push(@args, "RRA:AVERAGE:0.99999:1:576"); # 48 hours at 5 minute resolution + push(@args, "RRA:AVERAGE:0.99999:12:168"); # 1 week at 1 hour resolution + push(@args, "RRA:AVERAGE:0.99999:288:366"); # 1 year at 1 day resolution + RRDs::create($rrdfile, "--start", $startts, @args); + + my $ERR = RRDs::error; + if ($ERR) { + print "Error creating RRD file $rrdfile: $ERR\n"; + return; + } + } + + return $rrdfile; +} + +sub read_knownlinks { + open(KLFILE, $knownlinksfile) or die("Cannot open $knownlinksfile!"); + while () { + chomp; + next if (/(^\s*#)|(^\s*$)/); # empty line or comment + + my ($routerip,$ifindex,$tag,$descr,$color) = split(/\t+/); + $knownlinks{"${routerip}_${ifindex}"} = $tag; + } + close(KLFILE); +}