#!@PERL@ ## ## OSSP flow2rrd -- NetFlow to Round-Robin Database ## Copyright (c) 2004 Ralf S. Engelschall ## Copyright (c) 2004 The OSSP Project ## ## This file is part of OSSP flow2rrd, a tool for storing NetFlow data ## into an RRD which can be found at http://www.ossp.org/pkg/tool/flow2rrd/. ## ## This program is free software; you can redistribute it and/or modify ## it under the terms of the GNU General Public License as published by ## the Free Software Foundation; either version 2 of the License, or ## (at your option) any later version. ## ## This program is distributed in the hope that it will be useful, ## but WITHOUT ANY WARRANTY; without even the implied warranty of ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ## General Public License for more details. ## ## You should have received a copy of the GNU General Public License ## along with this program; if not, write to the Free Software ## Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, ## USA, or contact Ralf S. Engelschall . ## ## flow2rrd.pl: program (language: Perl) ## require 5.008; use strict; $|++; # external requirements use POSIX; # from OpenPKG "perl" use IO::File; # from OpenPKG "perl" use Getopt::Long; # from OpenPKG "perl" use Time::Local; # from OpenPKG "perl" use File::Temp; # from OpenPKG "perl" use Data::Dumper; # from OpenPKG "perl" use Date::Parse; # from OpenPKG "perl-time" use Net::Patricia; # from OpenPKG "perl-net" use String::Divert; # from OpenPKG "perl-text" use OSSP::cfg; # from OpenPKG "cfg" [with_perl=yes] use Cflow qw(); # from OpenPKG "flowtools" [with_perl=yes] use RRDs; # from OpenPKG "rrdtools" use CGI; # from OpenPKG "perl-www" # Data::Dumper configuration $Data::Dumper::Purity = 1; $Data::Dumper::Indent = 1; $Data::Dumper::Terse = 1; # fixed program information my $my = { -progname => 'OSSP flow2rrd', -proghome => 'http://www.ossp.org/pkg/tool/flow2rrd/', -progvers => '@VERSION@', }; # run-time options my $opt = { -help => 0, -version => 0, -config => '@SYSCONFDIR@/flow2rrd.cfg', -store => 0, -graph => 0, -cgi => (($ENV{'GATEWAY_INTERFACE'} || "") eq 'CGI/1.1' ? 1 : 0) }; # parse command line options Getopt::Long::Configure("bundling"); my %getopt_spec = ( 'h|help' => \$opt->{-help}, 'v|version' => \$opt->{-version}, 'V|verbose' => \$opt->{-verbose}, 'f|config=s' => \$opt->{-config}, 's|store' => \$opt->{-store}, 'g|graph' => \$opt->{-graph}, 'c|cgi' => \$opt->{-cgi}, ); my $result = GetOptions(%getopt_spec) or die "command line option parsing failed"; if ($opt->{help}) { print "usage: $my->{-progname} [] \n" . "available options are:\n" . " -h,--help print out this usage page\n" . " -v,--version print program version\n" . " -V,--verbose print verbose messages\n" . " -f,--config FILE read this configuration file only\n" . " -s,--store store NetFlow values into RRD\n" . " -g,--graph produce RRD graphs\n" . " -c,--cgi produce Web user interface\n"; exit(0); } if ($opt->{-version}) { print "$my->{-progname} $my->{-progvers}\n"; exit(0); } if (not $opt->{-store} and not $opt->{-graph} and not $opt->{-cgi}) { die "either --store, --graph or --cgi option has to be given"; } if (($opt->{-store} or $opt->{-graph}) and $opt->{-cgi}) { die "option --cgi cannot be combined with --store or --graph"; } # read configuration file my $io = new IO::File "<$opt->{-config}" or die "unable to read configuration file \"$opt->{-config}\""; my $txt; { local $/; $txt = <$io>; } $io->close(); # parse configuration files my $cs = new OSSP::cfg::simple; $cs->parse($txt); my $tree = $cs->unpack(); undef $cs; #print Data::Dumper->Dump([$tree]); # extract configuration elements my $cfg = { 'Database' => {}, 'Host' => [], 'Protocol' => {}, 'Service' => {}, 'Colors' => {}, }; foreach my $dir (@{$tree}) { if ($dir->[0] eq 'Database') { die "Database already defined" if (defined($cfg->{'Database'}->{-file})); $cfg->{'Database'}->{-file} = $dir->[1]; my $seq = $dir->[2]; foreach my $dir2 (@{$seq}) { if ($dir2->[0] eq 'Stepping') { $cfg->{'Database'}->{-step} = $dir2->[1]; } elsif ($dir2->[0] eq 'Storage') { my $s = []; foreach my $spec (@{$dir2}[1..$#{$dir2}]) { if (my ($res, $dur) = ($spec =~ m/^(\S+):(\S+)$/)) { push(@{$s}, { -res => $res, -dur => $dur}); } else { die "invalid storage specification \"$spec\""; } } $cfg->{'Database'}->{-storage} = $s; } } } elsif ($dir->[0] eq 'Protocol') { die "Protocol \"$dir->[1]\" already defined" if (defined($cfg->{'Protocol'}->{$dir->[1]})); die "invalid protocol number \"$dir->[2]\"" if ($dir->[2] !~ m|^\d+$|); $cfg->{'Protocol'}->{$dir->[1]} = $dir->[2]; } elsif ($dir->[0] eq 'Service') { die "Service \"$dir->[1]\" already defined" if (defined($cfg->{'Service'}->{$dir->[1]})); my $s = []; foreach my $spec (@{$dir}[2..$#{$dir}]) { if (my ($proto, $port) = ($spec =~ m/^(\S+):(\d+|\*)$/)) { die "invalid protocol number \"$proto\"" if (not ($proto =~ m|^\d+$| or defined($cfg->{'Protocol'}->{$proto}))); push(@{$s}, { -proto => $proto, -port => $port}); } else { die "invalid service specification \"$spec\""; } } $cfg->{'Service'}->{$dir->[1]} = $s; } elsif ($dir->[0] eq 'Host') { my $h = { -name => $dir->[1], -target => { -order => [] }, }; my $seq = $dir->[2]; foreach my $dir2 (@{$seq}) { if ($dir2->[0] eq 'Target') { my $t = { -network => [], -service => [] }; my $seq2 = $dir2->[2]; foreach my $dir3 (@{$seq2}) { if ($dir3->[0] eq 'Network') { $t->{-network} = [ @{$dir3}[1..$#{$dir3}] ]; } elsif ($dir3->[0] eq 'Service') { $t->{-service} = [ @{$dir3}[1..$#{$dir3}] ]; } else { die "invalid configuration directive \"$dir3->[0]\""; } } $h->{-target}->{$dir2->[1]} = $t; push(@{$h->{-target}->{-order}}, $dir2->[1]); } else { die "invalid configuration directive \"$dir2->[0]\""; } } push(@{$cfg->{'Host'}}, $h); } elsif ($dir->[0] eq 'Colors') { die "Colors \"$dir->[1]\" already defined" if (defined($cfg->{'Colors'}->{$dir->[1]})); my $c = []; my $last = 0x000000; foreach my $spec (@{$dir}[2..$#{$dir}]) { my $this; if ($spec =~ m|^[\da-fA-F]{6}$|) { push(@{$c}, $spec); $last = $spec; } elsif ($spec =~ m|^([-+])([\da-fA-F]{6})(?:\((\d+)\))?$|) { my ($op, $color, $repeat) = ($1, $2, $3); $repeat = 1 if (not defined($repeat)); for (my $i = 0; $i < $repeat; $i++) { my $this; eval "\$this = sprintf(\"%06x\", (0x$last $op 0x$color) % 0xffffff)"; push(@{$c}, $this); $last = $this; } } else { die "invalid color specification \"$spec\""; } } $cfg->{'Colors'}->{$dir->[1]} = $c; } else { die "invalid configuration directive \"$dir->[0]\""; } } #print Data::Dumper->Dump([$cfg]); # hostname/target/service to 15 chars RRD DS name mapping my $rrd_ds_name_cache = {}; sub make_rrd_ds_name { my ($host, $target, $service) = @_; my $ds_name = $rrd_ds_name_cache->{$host.$target.$service}; if (not defined($ds_name)) { $host =~ s|[^a-zA-Z0-9_-]||sg; $host = substr($host . ("_"x5), 0, 5); $target =~ s|[^a-zA-Z0-9_-]||sg; $target = substr($target . ("_"x5), 0, 5); $service =~ s|[^a-zA-Z0-9_-]||sg; $service = substr($service . ("_"x5), 0, 5); $ds_name = sprintf("%s_%s_%s", $host, $target, $service); $rrd_ds_name_cache->{$host.$target.$service} = $ds_name; } return $ds_name; } # conversion/canonicalization of time specifications sub cv_time { my ($t) = @_; if ($t =~ m|^now(.*)$|) { $t = time() + &cv_time($1); } elsif ($t =~ m|^([-+])(.+)$|) { $t = &cv_time($2); eval "\$t = $1 \$t"; } elsif ($t =~ m|(\d{2})-([A-Za-z]{3})-(\d{4})|) { $t = str2time($t); } elsif ($t =~ m|^([\d.]+)([smhDWMY])$|) { $t = $1; if ($2 eq 's') { $t *= 1; } elsif ($2 eq 'm') { $t *= 60; } elsif ($2 eq 'h') { $t *= 60*60; } elsif ($2 eq 'D') { $t *= 24*60*60; } elsif ($2 eq 'W') { $t *= 7*24*60*60; } elsif ($2 eq 'M') { $t *= 30*24*60*60; } elsif ($2 eq 'Y') { $t *= 365*24*60*60; } } elsif ($t =~ m|^([\d.]+)$|) { $t = $1; } else { $t = 0; } return $t; } # conversion/canonicalization of limit specifications sub cv_limit { my ($l) = @_; if ($l eq '') { $l = 0; } elsif ($l =~ m|^([-+])(.*)$|) { $l = &cv_limit($2); eval "\$l = $1 \$l"; } elsif ($l =~ m|^(\d+)([KMGT])$|) { $l = $1; if ($2 eq 'K') { $l *= 1024; } if ($2 eq 'M') { $l *= 1024*1024; } if ($2 eq 'G') { $l *= 1024*1024*1024; } if ($2 eq 'T') { $l *= 1024*1024*1024*1024; } } return $l; } ## ## ==== OPERATION MODE 1: STORE DATA ==== ## if ($opt->{-store}) { my $step = &cv_time($cfg->{'Database'}->{-step}); # initialize data my $ctx = &data_init($cfg); # scan flow-tools stream on STDIN for NetFlow records my $flows = 0; my $tick = 0; my $ticktick = 0; my $done = 0; my @done = (); Cflow::verbose(0); Cflow::find(sub { &foreach_record($cfg, $ctx) }, "-"); sub foreach_record { my ($cfg, $ctx) = @_; # at start of time slot, load accumulated data if (not defined($ctx->{-endtime})) { # initial setup, so initialize time slot tracking $ctx->{-starttime} = int($Cflow::endtime / $step) * $step; $ctx->{-endtime} = $ctx->{-starttime} + $step; # load data &rrd_load($cfg, $ctx); } # at end of time slot, store accumulated data if ($Cflow::endtime >= $ctx->{-endtime}) { # store data &rrd_store($cfg, $ctx); # step one time slot forward $ctx->{-starttime} = $ctx->{-endtime}; $ctx->{-endtime} += $step; } # accumulate data &data_accumulate($cfg, $ctx); # statistics if ($opt->{-verbose}) { $flows++; $done++; $ticktick++; my $tick_new = ($ticktick > 1000 ? time() : 0); if ($tick < $tick_new) { push(@done, $done); shift(@done) if (@done > 20); my $sum = 0; map { $sum += $_ } @done; $sum /= scalar(@done); printf(STDERR "Storing: %10d flows, %6.1f flows/sec (average: %6.1f flows/sec)\r", $flows, $done, $sum); $tick = $tick_new; $done = 0; } } } &rrd_store($cfg, $ctx); # initialize data tracking context sub data_init { my ($cfg) = @_; # create tracking context my $ctx = { -starttime => 0, -endtime => undef, -track => {}, -network => {}, }; foreach my $host (@{$cfg->{'Host'}}) { foreach my $target (grep { $_ !~ m/^-/ } keys(%{$host->{-target}})) { my $np = new Net::Patricia; foreach my $network (@{$host->{-target}->{$target}->{-network}}) { $np->add_string($network, 1); } $ctx->{-network}->{$host->{-name}.":".$target} = $np; foreach my $service (@{$host->{-target}->{$target}->{-service}}) { my $ds_name = &make_rrd_ds_name($host->{-name}, $target, $service); $ctx->{-track}->{"${ds_name}_i"} = 0; $ctx->{-track}->{"${ds_name}_o"} = 0; } } } return $ctx; } # create RRD file (if still not existing) sub rrd_create { my ($cfg, $time) = @_; return if (-f $cfg->{'Database'}->{-file}); # determine RRD data sources (DS) my @ds = (); foreach my $host (@{$cfg->{'Host'}}) { foreach my $target (grep { $_ !~ m/^-/ } keys(%{$host->{-target}})) { foreach my $service (@{$host->{-target}->{$target}->{-service}}) { my $ds_name = &make_rrd_ds_name($host->{-name}, $target, $service); push(@ds, "DS:${ds_name}_i:ABSOLUTE:600:0:U"); push(@ds, "DS:${ds_name}_o:ABSOLUTE:600:0:U"); } } } push(@ds, "DS:UNKNOWN:ABSOLUTE:600:0:U"); # determine RRD archive (RRA) my @rra = (); sub mkrra { my ($step, $res, $duration) = @_; my $steps = int($res / $step); my $rows = int($duration / $res); my $rra = sprintf('RRA:LAST:0:%d:%d', $steps, $rows); push(@rra, $rra); } foreach my $s (@{$cfg->{'Database'}->{-storage}}) { my $res = &cv_time($s->{-res}); my $dur = &cv_time($s->{-dur}); &mkrra($step, $res, $dur); } # create RRD database RRDs::create($cfg->{'Database'}->{-file}, '--start', $time, '--step', $step, @ds, @rra); my $err = RRDs::error(); die "failed to create RRD file: $err" if (defined($err)); } # load already stored accumulated data of current time slot back from RRD sub rrd_load { my ($cfg, $ctx) = @_; # make sure the RRD is available &rrd_create($cfg, $Cflow::endtime); # load data from RRD my ($rrd_start, $rrd_step, $rrd_names, $rrd_data) = RRDs::fetch( $cfg->{'Database'}->{-file}, 'LAST', '--resolution', $step, '--start', $ctx->{-endtime}, '--end', $ctx->{-endtime} ); my $err = RRDs::error(); if (not defined($err) and defined($rrd_names) and defined($rrd_data)) { for (my $i = 0; $i <= $#{$rrd_names}; $i++) { my $ds_name = $rrd_names->[$i]; my $ds_value = $rrd_data->[0]->[$i] || 0; $ctx->{-track}->{$ds_name} = $ds_value; } } } # accumulate data sub data_accumulate { my ($cfg, $ctx) = @_; # iterate over all target and services to see whether # the flow matches them... my $matched_total = 0; LOOP: foreach my $host (@{$cfg->{'Host'}}) { foreach my $target (@{$host->{-target}->{-order}}) { my $inbound; my $np = $ctx->{-network}->{$host->{-name}.":".$target}; if ($np->match_string($Cflow::srcip)) { $inbound = 0; } elsif ($np->match_string($Cflow::dstip)) { $inbound = 1; } if (defined($inbound)) { foreach my $service (@{$host->{-target}->{$target}->{-service}}) { foreach my $s (@{$cfg->{'Service'}->{$service}}) { if ($Cflow::protocol == $cfg->{'Protocol'}->{$s->{-proto}}) { my $port = $s->{-port}; if ( $port eq '*' or (( $inbound and $port == $Cflow::dstport) or (not $inbound and $port == $Cflow::srcport))) { # flow matched target/service, so accumulate data my $ds_name = &make_rrd_ds_name($host->{-name}, $target, $service); if ($inbound) { $ctx->{-track}->{"${ds_name}_i"} += $Cflow::bytes; } else { $ctx->{-track}->{"${ds_name}_o"} += $Cflow::bytes; } $matched_total++; last LOOP; } } } } } } } if ($matched_total == 0) { $ctx->{-track}->{"UNKNOWN"} += $Cflow::bytes; } } # store accumulated data into RRD sub rrd_store { my ($cfg, $ctx) = @_; # make sure the RRD is available &rrd_create($cfg, $Cflow::endtime); # store data to RRD my $ds_list = ''; my $dv_list = ''; my $i = 0; foreach my $ds_name (sort(keys(%{$ctx->{-track}}))) { # generate update argument $ds_list .= ($ds_list ne '' ? ":" : "") . $ds_name; $dv_list .= ($dv_list ne '' ? ":" : "") . $ctx->{-track}->{$ds_name}; # reset value $ctx->{-track}->{$ds_name} = 0; } RRDs::update( $cfg->{'Database'}->{-file}, '--template', $ds_list, sprintf("%d", $ctx->{-endtime}).":".$dv_list ); my $err = RRDs::error(); warn "failed to store data to RRD file: $err" if (defined($err)); } } ## ## ==== OPERATION MODE 2: GENERATE GRAPHS ==== ## if ($opt->{-graph}) { if (@ARGV == 0) { die "missing graph specifications"; } foreach my $spec (@ARGV) { # determine graph parameters $spec =~ m/^(.+)\@(\S+):(\d+):(\d+):([^:]+):([^:]+):([^:]+):([^:]+)$/ or die "invalid graph specification \"$spec\" (expect :::::::)"; my ($content, $img_file, $img_width, $img_height) = ($1, $2, $3, $4); my ($graph_start, $graph_end, $graph_ulimit, $graph_llimit) = ($5, $6, $7, $8); # post-process parameters my $img_format = ($img_file =~ m|\.png$| ? "PNG" : "GIF"); if ($graph_start =~ m/^\-(.+)/) { $graph_end = &cv_time($graph_end); $graph_start = $graph_end - &cv_time($1); } elsif ($graph_end =~ m/^\+(.+)/) { $graph_start = &cv_time($graph_start); $graph_end = $graph_start + &cv_time($1); } else { $graph_start = &cv_time($graph_start); $graph_end = &cv_time($graph_end); } $graph_ulimit = &cv_limit($graph_ulimit); $graph_llimit = &cv_limit($graph_llimit); my $graph = { -img_file => $img_file, -img_format => $img_format, -img_width => $img_width, -img_height => $img_height, -graph_start => $graph_start, -graph_end => $graph_end, -graph_ulimit => $graph_ulimit, -graph_llimit => $graph_llimit, }; if ($content =~ m|^(\S+):(\S+)$|) { &make_graph_target($graph, $1, $2); } else { &make_graph_host($graph, $content); } # generate graph for a host sub make_graph_host { my ($graph, $hostname) = @_; # find host configuration record my $host; $host = undef; foreach my $h (@{$cfg->{'Host'}}) { if ($h->{-name} eq $hostname) { $host = $h; last; } } if (not defined($host)) { die "host \"$hostname\" not found"; } my $colors = $cfg->{'Colors'}->{'colorful'}; # FIXME my @def = (); my @cdef = (); my @draw_o = (); my @draw_i = (); my $i = 0; my $cols = 1; $cols = 2 if ($graph->{-img_width} >= 200); $cols = 3 if ($graph->{-img_width} >= 400); $cols = 4 if ($graph->{-img_width} >= 600); $cols = 5 if ($graph->{-img_width} >= 800); # FIXME: UNKNOWN data? my $data_i = ''; my $data_o = ''; foreach my $target (@{$host->{-target}->{-order}}) { my $cdef_i = ''; my $cdef_o = ''; foreach my $service (@{$host->{-target}->{$target}->{-service}}) { my $ds_name = &make_rrd_ds_name($host->{-name}, $target, $service); push(@def, sprintf("DEF:%s_o=%s:%s_o:LAST", $ds_name, $cfg->{'Database'}->{-file}, $ds_name)); push(@def, sprintf("DEF:%s_i=%s:%s_i:LAST", $ds_name, $cfg->{'Database'}->{-file}, $ds_name)); $cdef_o = ($cdef_o eq '' ? "${ds_name}_o" : "${ds_name}_o,$cdef_o,+"); $cdef_i = ($cdef_i eq '' ? "${ds_name}_i" : "${ds_name}_i,$cdef_i,+"); } $cdef_o .= ",8,*"; $cdef_i .= ",8,*"; $cdef_i .= ",-1,*"; push(@cdef, sprintf("CDEF:data%d_o=%s", $i, $cdef_o)); push(@cdef, sprintf("CDEF:data%d_i=%s", $i, $cdef_i)); my $color_o; eval "\$color_o = 0x".$colors->[$i]; my $color_i; eval "\$color_i = \$color_o - 0x101010"; push(@draw_o, sprintf("GPRINT:data%d_o:AVERAGE:%%4.0lf%%S", $i)); push(@draw_o, sprintf("%s:data%d_o#%06x:out %s", ($i == 0 ? "AREA" : "STACK"), $i, $color_o, sprintf("%-8s", substr($target, 0, 8)))); push(@draw_o, 'COMMENT:\n') if ($i % $cols == ($cols-1)); push(@draw_i, sprintf("GPRINT:data%d_i:AVERAGE:%%4.0lf%%S", $i)); push(@draw_i, sprintf("%s:data%d_i#%06x:in %s", ($i == 0 ? "AREA" : "STACK"), $i, $color_i, sprintf("%-8s", substr($target, 0, 8)))); push(@draw_i, 'COMMENT:\n') if ($i % $cols == ($cols-1)); $data_o = ($data_o eq '' ? sprintf("data%d_o", $i) : sprintf("data%d_o,%s,+", $i, $data_o)); $data_i = ($data_i eq '' ? sprintf("data%d_i", $i) : sprintf("data%d_i,%s,+", $i, $data_i)); $i++; } push(@cdef, sprintf("CDEF:data_o=%s", $data_o)); push(@cdef, sprintf("CDEF:data_i=%s", $data_i)); my @draw = (@draw_o, 'COMMENT:\n', @draw_i); push(@draw, 'COMMENT:\n'); push(@draw, 'COMMENT:\n'); push(@draw, sprintf("GPRINT:data_o:AVERAGE:Total Average Traffic\\: %%.0lf%%S out /")); push(@draw, sprintf("GPRINT:data_i:AVERAGE:%%.0lf%%S in")); push(@draw, "HRULE:0#000000"); my $now = time(); my $tzoffset = $now - timelocal(gmtime($now)); my $ts = int(($graph->{-graph_start} / (24*60*60)) + 0) * (24*60*60); my $te = int(($graph->{-graph_end} / (24*60*60)) + 1) * (24*60*60); for (my $t = $ts; $t < $te; $t += (12*60*60)) { if (($t % (24*60*60)) == 0) { push(@draw, sprintf("VRULE:%d#000000", $t - $tzoffset)); } else { push(@draw, sprintf("VRULE:%d#999999", $t - $tzoffset)); } } my @args = (); push(@args, $graph->{-img_file}); push(@args, '--imgformat', $graph->{-img_format}); push(@args, '--width', $graph->{-img_width}); push(@args, '--height', $graph->{-img_height}); push(@args, '--start', $graph->{-graph_start}); push(@args, '--end', $graph->{-graph_end}); push(@args, '--upper-limit', $graph->{-graph_ulimit}) if ($graph->{-graph_ulimit} != 0); push(@args, '--lower-limit', $graph->{-graph_llimit}) if ($graph->{-graph_llimit} != 0); push(@args, '--rigid'); push(@args, '--alt-autoscale'); push(@args, '--base', 1024); push(@args, '--x-grid', "HOUR:1:DAY:1:DAY:1:0:%d-%b-%Y"); push(@args, '--vertical-label', 'Bit/s'); push(@args, '--color', 'CANVAS#f0f0ff'); push(@args, '--color', 'BACK#e0e0f0'); push(@args, '--color', 'SHADEA#e5e5f5'); push(@args, '--color', 'SHADEB#d0d0e0'); push(@args, '--color', 'GRID#cccccc'); push(@args, '--color', 'MGRID#999999'); push(@args, '--color', 'FONT#000000'); push(@args, '--color', 'FRAME#000000'); push(@args, '--color', 'ARROW#000000'); push(@args, '--title', sprintf("Host %s (+out/-in)", $host->{-name})); push(@args, @def); push(@args, @cdef); push(@args, @draw); my ($rrd_averages, $rrd_xsize, $rrd_ysize) = &rrd_graph(@args); my $err = RRDs::error(); die "failed to generate graph from RRD file: $err" if (defined($err)); } # generate graph for a target sub make_graph_target { my ($graph, $hostname, $targetname) = @_; # find host configuration record my $host; $host = undef; foreach my $h (@{$cfg->{'Host'}}) { if ($h->{-name} eq $hostname) { $host = $h; last; } } if (not defined($host)) { die "host \"$hostname\" not found"; } # find target configuration record my $target; $target = undef; foreach my $t (@{$host->{-target}->{-order}}) { if ($t eq $targetname) { $target = $t; last; } } if (not defined($target)) { die "target \"$targetname\" not found"; } my $colors = $cfg->{'Colors'}->{'colorful'}; # FIXME my @def = (); my @cdef = (); my @draw_o = (); my @draw_i = (); my $i = 0; my $cols = 1; $cols = 2 if ($graph->{-img_width} >= 200); $cols = 3 if ($graph->{-img_width} >= 400); $cols = 4 if ($graph->{-img_width} >= 600); $cols = 5 if ($graph->{-img_width} >= 800); my $data_i = ''; my $data_o = ''; foreach my $service (@{$host->{-target}->{$target}->{-service}}) { my $ds_name = &make_rrd_ds_name($host->{-name}, $target, $service); push(@def, sprintf("DEF:%s_o=%s:%s_o:LAST", $ds_name, $cfg->{'Database'}->{-file}, $ds_name)); push(@def, sprintf("DEF:%s_i=%s:%s_i:LAST", $ds_name, $cfg->{'Database'}->{-file}, $ds_name)); push(@cdef, sprintf("CDEF:data%d_o=%s_o,8,*,+1,*", $i, $ds_name)); push(@cdef, sprintf("CDEF:data%d_i=%s_i,8,*,-1,*", $i, $ds_name)); my $color_o; eval "\$color_o = 0x".$colors->[$i]; my $color_i; eval "\$color_i = \$color_o - 0x101010"; push(@draw_o, sprintf("GPRINT:data%d_o:AVERAGE:%%4.0lf%%s", $i)); push(@draw_o, sprintf("%s:data%d_o#%06x:out %s", ($i == 0 ? "AREA" : "STACK"), $i, $color_o, sprintf("%-8s", substr($service, 0, 8)))); push(@draw_o, 'COMMENT:\n') if ($i % $cols == ($cols-1)); push(@draw_i, sprintf("GPRINT:data%d_i:AVERAGE:%%4.0lf%%s", $i)); push(@draw_i, sprintf("%s:data%d_i#%06x:in %s", ($i == 0 ? "AREA" : "STACK"), $i, $color_i, sprintf("%-8s", substr($service, 0, 8)))); push(@draw_i, 'COMMENT:\n') if ($i % $cols == ($cols-1)); $data_o = ($data_o eq '' ? sprintf("data%d_o", $i) : sprintf("data%d_o,%s,+", $i, $data_o)); $data_i = ($data_i eq '' ? sprintf("data%d_i", $i) : sprintf("data%d_i,%s,+", $i, $data_i)); $i++; } push(@cdef, sprintf("CDEF:data_o=%s", $data_o)); push(@cdef, sprintf("CDEF:data_i=%s", $data_i)); my @draw = (@draw_o, 'COMMENT:\n', @draw_i); push(@draw, 'COMMENT:\n'); push(@draw, 'COMMENT:\n'); push(@draw, sprintf("GPRINT:data_o:AVERAGE:Total Average Traffic\\: %%.0lf%%S out /")); push(@draw, sprintf("GPRINT:data_i:AVERAGE:%%.0lf%%S in")); push(@draw, "HRULE:0#000000"); my $now = time(); my $tzoffset = $now - timelocal(gmtime($now)); my $ts = int(($graph->{-graph_start} / (24*60*60)) + 0) * (24*60*60); my $te = int(($graph->{-graph_end} / (24*60*60)) + 1) * (24*60*60); for (my $t = $ts; $t < $te; $t += (12*60*60)) { if (($t % (24*60*60)) == 0) { push(@draw, sprintf("VRULE:%d#000000", $t - $tzoffset)); } else { push(@draw, sprintf("VRULE:%d#999999", $t - $tzoffset)); } } my @args = (); push(@args, $graph->{-img_file}); push(@args, '--imgformat', $graph->{-img_format}); push(@args, '--width', $graph->{-img_width}); push(@args, '--height', $graph->{-img_height}); push(@args, '--start', $graph->{-graph_start}); push(@args, '--end', $graph->{-graph_end}); push(@args, '--upper-limit', $graph->{-graph_ulimit}) if ($graph->{-graph_ulimit} != 0); push(@args, '--lower-limit', $graph->{-graph_llimit}) if ($graph->{-graph_llimit} != 0); push(@args, '--rigid'); push(@args, '--alt-autoscale'); push(@args, '--base', 1024); push(@args, '--x-grid', "HOUR:1:DAY:1:DAY:1:0:%d-%b-%Y"); push(@args, '--vertical-label', 'Bit/s'); push(@args, '--color', 'CANVAS#f0f0ff'); push(@args, '--color', 'BACK#e0e0f0'); push(@args, '--color', 'SHADEA#e5e5f5'); push(@args, '--color', 'SHADEB#d0d0e0'); push(@args, '--color', 'GRID#cccccc'); push(@args, '--color', 'MGRID#999999'); push(@args, '--color', 'FONT#000000'); push(@args, '--color', 'FRAME#000000'); push(@args, '--color', 'ARROW#000000'); push(@args, '--title', sprintf("Target %s on Host %s (+out/-in)", $target, $host->{-name})); push(@args, @def); push(@args, @cdef); push(@args, @draw); my ($rrd_averages, $rrd_xsize, $rrd_ysize) = &rrd_graph(@args); my $err = RRDs::error(); die "failed to generate graph from RRD file: $err" if (defined($err)); } # render an RRD graph (frontend to RRDs::graph() function) sub rrd_graph { my (@args) = @_; my ($rrd_results, $rrd_xsize, $rrd_ysize); # if no Y axis limits are specified, try to determine # reasonable ones based on the calculated average values # (instead of the maximum values RRDTool uses by default) if (not grep { $_ =~ m/^--(?:upper|lower)-limit$/ } @args) { my @a = @args; @a = map { s/^GPRINT/PRINT/s; $_ } grep { $_ !~ m/^(AREA|STACK|LINE|HRULE|VRULE):/ } @a; ($rrd_results, $rrd_xsize, $rrd_ysize) = RRDs::graph(@a); my $err = RRDs::error(); if (not defined($err)) { my $print = join(" ", @{$rrd_results}); if ($print =~ m/Total Average Traffic:\s+(\S+)\s+out\s+\/\s+(\S+)\s+in/s) { my ($ulimit, $llimit) = (&canon($1), &canon($2)); sub canon { my ($limit) = @_; if ($limit =~ m|^([+-]?\d+)k$|) { $limit = $1 * 1000; } elsif ($limit =~ m|^([+-]?\d+)M$|) { $limit = $1 * 1000*1000; } elsif ($limit =~ m|^([+-]?\d+)G$|) { $limit = $1 * 1000*1000*1000; } return $limit; } $ulimit = int($ulimit * 1.5); $llimit = int($llimit * 1.5); push(@args, '--upper-limit', $ulimit); push(@args, '--lower-limit', $llimit); } } } # pass through the arguments to the RRDs::graph() function ($rrd_results, $rrd_xsize, $rrd_ysize) = RRDs::graph(@args); return ($rrd_results, $rrd_xsize, $rrd_ysize); } } } ## ## ==== OPERATION MODE 3: GENERATE WEB USER INTERFACE ==== ## if ($opt->{-cgi}) { my $cgi = new CGI; # CGI error handler $SIG{__DIE__} = sub { my ($msg) = @_; my $hint = ''; if ($msg =~ m|line\s+(\d+)|) { my $line = $1; my $io = new IO::File "<$0"; my @code = $io->getlines(); $io->close(); my $i = -1; $hint = join("", map { s/^/sprintf("%d: ", $line+$i++)/se; $_; } @code[$line-2..$line]); } print STDOUT "Content-Type: text/html; charset=ISO-8859-1\n" . "\n" . "\n" . " \n" . " OSSP flow2rrd: ERROR\n" . " \n" . " \n" . "

OSSP flow2rrd: ERROR

\n" . "

\n" . " \n" . " $msg
\n" . "
\n" . "

\n$hint
\n" . " \n" . "\n"; exit(0); }; if (defined($cgi->param("css"))) { # # output Cascading Style Sheet (CSS) # # define CSS content my $css = q{ BODY { background: #c0c0c0; color: #ffffff; font-family: helvetica,arial,tahoma,verdana,sans-serif; } TABLE.flow2rrd { background: #000000; border: 2px solid #000000; } TABLE.flow2rrd TD.header { color: #ffffff; font-family: tahoma,helvetica,arial,tahoma,verdana,sans-serif; font-weight: bold; font-size: 200%; padding: 4px; text-align: center; } TABLE.flow2rrd TD.header A { text-decoration: none; color: #ffffff; } TABLE.flow2rrd TD.footer { color: #ffffff; font-family: tahoma,helvetica,arial,tahoma,verdana,sans-serif; padding: 4px; text-align: center; } TABLE.flow2rrd TD.footer A { text-decoration: none; font-weight: bold; color: #ffffff; } TABLE.flow2rrd TABLE.explore TD.toolbar { background: #333333; padding: 10px; } TABLE.flow2rrd TABLE.explore TD.toolbar INPUT.textfield { background: #333333; color: #ffffff; border: 0px; border-bottom: 1px solid #999999; } TABLE.flow2rrd TABLE.explore TD.toolbar INPUT.submit { background: #666666; color: #ffffff; margin-top: 10px; border: 1px solid #999999; font-weight: bold; width: 100%; } }; # send out CSS data $css =~ s|^ ||mg; print STDOUT $cgi->header( -type => 'text/css', -content_length => length($css), -expires => '+5m' ) . $css; } elsif (defined(my $graph = $cgi->param("graph"))) { # # output graph image # # prepare graph generation my (undef, $tmpfile) = mkstemp(($ENV{'TMPDIR'} || '/tmp') . "/flow2rrd.XXXXXX"); $graph =~ s|\@|\@$tmpfile:|s; # generate graph image my $rc = system("GATEWAY_INTERFACE=none $0 --config=\"$opt->{-config}\" --graph $graph"); if ($rc != 0 or not -s $tmpfile) { die "failed to generate graph image: $!"; } # read graph image my $io = new IO::File "<$tmpfile" or die "cannot read graph"; my $data; { local $/; $data = <$io>; } $io->close(); # send out graph image print STDOUT $cgi->header( -type => 'image/png', -content_length => length($data), -expires => '+5m' ) . $data; # cleanup unlink($tmpfile) } elsif (defined(my $explore = $cgi->param("explore"))) { # # output HTML page: EXPLORE A GRAPH # # generate HTML page diversion my $html = new String::Divert; $html->overload(1); # generate HTML page skeleton $html .= "\n" . " \n" . " " . $html->folder("head") . " \n" . " \n" . " " . $html->folder("body") . " \n" . "\n"; # generate HTML header $html >> "head"; $html .= "OSSP flow2rrd: Real-Time Network Statistics\n"; $html .= "url(-relative => 1)."?css=1\">\n"; $html << 1; # generate HTML body page skeleton $html >> "body"; $html .= "\n"; $html .= " \n"; $html .= " \n"; $html .= " \n"; $html .= " \n"; $html .= " \n"; $html .= " \n"; $html .= " \n"; $html .= " \n"; $html .= " \n"; $html .= "
\n"; $html .= " " . $html->folder("header"); $html .= "
\n"; $html .= " " . $html->folder("canvas"); $html .= "
\n"; $html .= " " . $html->folder("footer"); $html .= "
\n"; $html << 1; # generate page header & footer $html >> "header"; $html .= "url(-relative => 1)."\">Real-Time Network Statistics"; $html << 1; $html >> "footer"; $html .= "{-proghome}\">$my->{-progname} $my->{-progvers}"; $html << 1; # determine input parameters (and their defaults) my $width = ($cgi->param("width") || "800"); my $height = ($cgi->param("height") || "200"); my $start = ($cgi->param("start") || "-48h"); my $end = ($cgi->param("end") || "now"); my $ulimit = ($cgi->param("ulimit") || "0"); my $llimit = ($cgi->param("llimit") || "0"); # generate page canvas skeleton $html >> "canvas"; $html .= $cgi->start_form( -method => "POST", -action => $cgi->url(-relative => 1) . "?explore=$explore", -enctype => "application/x-www-form-urlencoded" ); $html .= $cgi->hidden(-name => "explore", -default => $cgi->url(-relative => 1) . "?explore=$explore")."\n"; $html .= "\n"; $html .= " \n"; $html .= " \n"; $html .= " \n"; $html .= " \n"; $html .= " \n"; $html .= " \n"; $html .= "
\n"; $html .= " " . $html->folder("view"); $html .= "
\n"; $html .= " " . $html->folder("toolbar"); $html .= "
\n"; $html .= $cgi->end_form(); $html << 1; # generate page view part my $img = $cgi->url(-relative => 1) . "?graph=$explore\@$width:$height:$start:$end:$ulimit:$llimit"; $html >> "view"; $html .= "\n"; $html << 1; # generate page toolbar part $html >> "toolbar"; $html .= "\n"; $html .= ""; $html .= ""; $html .= ""; $html .= " \n"; $html .= " \n"; $html .= " \n"; $html .= "
Graph Size:" . $cgi->textfield( -name => 'width', -default => $width, -size => 15, -maxlength => 4, -class => 'textfield', ) . "x" . $cgi->textfield( -name => 'height', -default => $height, -size => 15, -maxlength => 4, -class => 'textfield', ) . "(pixels)Examples: '400 x 100', '800 x 200', ...
Data X-Range:" . $cgi->textfield( -name => 'start', -default => $start, -size => 15, -maxlength => 20, -class => 'textfield', ) . "-" . $cgi->textfield( -name => 'end', -default => $end, -size => 15, -maxlength => 20, -class => 'textfield', ) . "(time)Examples: '-2d - now', '24-Dec-2004 - +48h', ...
Data Y-Range:" . $cgi->textfield( -name => 'ulimit', -default => $ulimit, -size => 15, -maxlength => 10, -class => 'textfield', ) . "-" . $cgi->textfield( -name => 'llimit', -default => $llimit, -size => 15, -maxlength => 10, -class => 'textfield', ) . "(Bit/s)Examples: '2K - -1K', '4M - 2M', ...
". $cgi->submit(-name => "Update Graph", -class => "submit") . "
\n"; $html << 1; # send out page $html->undivert(0); print STDOUT $cgi->header( -type => 'text/html', -content_length => length($html), -expires => '+5m' ) . $html; # cleanup undef $html; } else { # # output HTML page: TOP-LEVEL SUMMARY # # generate HTML page diversion my $html = new String::Divert; $html->overload(1); # generate HTML page skeleton $html .= "\n" . " \n" . " " . $html->folder("head") . " \n" . " \n" . " " . $html->folder("body") . " \n" . "\n"; # generate HTML header $html >> "head"; $html .= "OSSP flow2rrd: Real-Time Network Statistics\n"; $html .= "url(-relative => 1)."?css=1\">\n"; $html << 1; # generate HTML body page skeleton $html >> "body"; $html .= "\n"; $html .= " \n"; $html .= " \n"; $html .= " \n"; $html .= " \n"; $html .= " \n"; $html .= " \n"; $html .= " \n"; $html .= " \n"; $html .= " \n"; $html .= "
\n"; $html .= " " . $html->folder("header"); $html .= "
\n"; $html .= " " . $html->folder("canvas"); $html .= "
\n"; $html .= " " . $html->folder("footer"); $html .= "
\n"; $html << 1; # generate page header & footer $html >> "header"; $html .= "url(-relative => 1)."\">Real-Time Network Statistics"; $html << 1; $html >> "footer"; $html .= "{-proghome}\">$my->{-progname} $my->{-progvers}"; $html << 1; # generate page canvas structure $html >> "canvas"; $html .= "\n"; $html .= " \n"; for (my $i = 0; $i < @{$cfg->{'Host'}}; $i++) { $html .= " \n"; } $html .= " \n"; $html .= "
\n"; $html .= " " . $html->folder("col$i"); $html .= "
\n"; $html << 1; # generate page canvas cells for (my $i = 0; $i < @{$cfg->{'Host'}}; $i++) { my $host = $cfg->{'Host'}->[$i]; $html >> "col$i"; $html .= "\n"; $html .= " \n"; $html .= " \n"; $html .= " \n"; foreach my $target (@{$host->{-target}->{-order}}) { $html .= " \n"; $html .= " \n"; $html .= " \n"; } $html .= "
\n"; my $url = $cgi->url(-relative => 1) . "?explore=$host->{-name}"; my $img = $cgi->url(-relative => 1) . "?graph=$host->{-name}\@400:100:-48h:now:0:0"; $html .= " \n"; $html .= "
\n"; my $url = $cgi->url(-relative => 1) . "?explore=$host->{-name}:$target"; my $img = $cgi->url(-relative => 1) . "?graph=$host->{-name}:$target\@400:100:-48h:now:0:0"; $html .= " \n"; $html .= "
\n"; $html << 1; } # send out page $html->undivert(0); print STDOUT $cgi->header( -type => 'text/html', -content_length => length($html), -expires => '+5m' ) . $html; # cleanup undef $html; } } # die gracefully exit(0);