Index: ossp-pkg/flow2rrd/flow2rrd.cfg RCS File: /v/ossp/cvs/ossp-pkg/flow2rrd/flow2rrd.cfg,v co -q -kk -p'1.1' '/v/ossp/cvs/ossp-pkg/flow2rrd/flow2rrd.cfg,v' | diff -u /dev/null - -L'ossp-pkg/flow2rrd/flow2rrd.cfg' 2>/dev/null --- ossp-pkg/flow2rrd/flow2rrd.cfg +++ - 2024-05-15 09:02:06.130182579 +0200 @@ -0,0 +1,80 @@ +## +## flow2rrd.cfg -- flow2rrd(1) configuration +## + +# Round-Robin Database +Database /tmp/flow2rrd.rrd; + +# Protocol Definitions +Protocol icmp 1; +Protocol tcp 6; +Protocol udp 17; +Protocol vrrp 112; + +# Service Definitions +Service icmp icmp:*; +Service vrrp vrrp:*; +Service ftp tcp:20 tcp:21; +Service ssh tcp:22; +Service telnet tcp:23; +Service smtp tcp:25; +Service dns udp:53 tcp:53; +Service tftp udp:69; +Service http tcp:80; +Service pop3 tcp:110; +Service sunrpc udp:111 tcp:111; +Service ident tcp:113; +Service nntp tcp:119; +Service ntp udp:123 tcp:123; +Service imap tcp:142 tcp:143; +Service snmp udp:161 udp:162; +Service ldap tcp:389; +Service https tcp:443; +Service syslog udp:514; +Service uucp tcp:540; +Service nntps tcp:563; +Service ldaps tcp:636; +Service rsync tcp:873; +Service ftps tcp:989 tcp:990; +Service imaps tcp:993; +Service pop3s tcp:995; +Service radius udp:1645 udp:1646 udp:1812 udp:1813; +Service irc tcp:194 tcp:6665 tcp:6666 tcp:6667 tcp:6668 tcp:6669; + +# Host Definitions +Host en2.engelschall.com { + Target en2.engelschall.com { + Network 195.30.6.130/32; + Service icmp vrrp dns ntp ssh smtp; + }; + Target visp1.engelschall.com { + Network 195.30.6.140/32 195.30.6.144/32; + Service dns smtp uucp; + }; + Target lotmjd1.lotterer.net { + Network 195.30.6.148/32; + Service dns smtp; + }; +}; +Host en3.engelschall.com { + Target en3.engelschall.com { + Network 195.30.6.132/32; + Service icmp vrrp dns ntp ssh smtp; + }; + Target visp2.engelschall.com { + Network 195.30.6.141/32 195.30.6.145/32; + Service dns smtp uucp; + }; + Target lotmjd2.lotterer.net { + Network 195.30.6.149/32; + Service dns smtp; + }; +}; + +# Color Themes +Colors colorful 666699 9999cc cc3333; +Colors grey f0f0f0 -101010(15); +Colors red cc3333 -101010(3) 000000; +Colors green ccffcc -333333(3) 000000; +Colors blue ccccff -333333(3) 000000; + Index: ossp-pkg/flow2rrd/flow2rrd.pl RCS File: /v/ossp/cvs/ossp-pkg/flow2rrd/flow2rrd.pl,v co -q -kk -p'1.1' '/v/ossp/cvs/ossp-pkg/flow2rrd/flow2rrd.pl,v' | diff -u /dev/null - -L'ossp-pkg/flow2rrd/flow2rrd.pl' 2>/dev/null --- ossp-pkg/flow2rrd/flow2rrd.pl +++ - 2024-05-15 09:02:06.132884653 +0200 @@ -0,0 +1,754 @@ +#!/usr/lpkg/bin/perl +## +## flow2rrd -- store NetFlow data in Round-Robin Database (RRD) +## Copyright (c) 2004 Ralf S. Engelschall +## + +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 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 => 'flow2rrd', + -progvers => '0.1.0', + -progdate => '20-Dec-2004', +}; + +# run-time options +my $opt = { + -help => 0, + -version => 0, + -verbose => 0, + -config => 'flow2rrd.cfg', + -store => 0, + -graph => 0, + -cgi => 0, +}; + +# parse command line options +Getopt::Long::Configure("bundling"); +my %getopt_spec = ( + 'h|help' => \$opt->{-help}, + 'V|version' => \$opt->{-version}, + 'v|verbose' => \$opt->{-verbose}, + 'c|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,--verbose enable verbose run-time mode\n" . + " -V,--version print program version\n" . + " -c,--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} ($my->{-progdate})\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; + +# extract configuration elements +my $cfg = { + 'Database' => undef, + 'Host' => [], + 'Protocol' => {}, + 'Service' => {}, + 'Colors' => {}, +}; +foreach my $dir (@{$tree}) { + if ($dir->[0] eq 'Database') { + die "Database already defined" if (defined($cfg->{'Database'})); + $cfg->{'Database'} = $dir->[1]; + } + 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 +sub make_rrd_ds_name { + my ($host, $target, $service) = @_; + $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); + my $ds_name = sprintf("%s_%s_%s", $host, $target, $service); + return $ds_name; +} + +## +## ==== OPERATION MODE 1: STORE DATA ==== +## + +if ($opt->{-store}) { + my $step = 5*60; # 5 min + + # initialize data + my $ctx = &data_init($cfg); + + # scan flow-tools stream on STDIN for NetFlow records + Cflow::verbose(1); + Cflow::find(sub { &foreach_record($cfg, $ctx) }, "-"); + sub foreach_record { + my ($cfg, $ctx) = @_; + + # determine time slot + my $t = $Cflow::endtime; + if (not defined($ctx->{-endtime})) { + # initial setup, so initialize time slot tracking + $ctx->{-starttime} = int($t / $step) * $step; + $ctx->{-endtime} = $ctx->{-starttime} + $step; + + # load data + &rrd_load($cfg, $ctx); + } + + # at end of time slot, store accumulated data + if ($t >= $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); + } + &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'}); + + # 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); + } + &mkrra($step, 5*60, 7*24*60*60); # 5 min res. for 1 week + &mkrra($step, 10*60, 14*24*60*60); # 10 min res. for 2 weeks + &mkrra($step, 30*60, 30*24*60*60); # 30 min res. for 1 month + &mkrra($step, 60*60, 3*30*24*60*60); # 1 hour res. for 3 months + &mkrra($step, 3*60*60, 6*30*24*60*60); # 3 hour res. for 6 months + &mkrra($step, 6*60*60, 365*24*60*60); # 6 hour res. for 1 year + &mkrra($step, 24*60*60, 4*365*24*60*60); # 1 days res. for 4 year + + # create RRD database + RRDs::create($cfg->{'Database'}, '--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'}, + '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) = @_; + + #my $st = POSIX::ctime($Cflow::startime); $st =~ s/\r?\n$//s; + #my $et = POSIX::ctime($Cflow::endtime); $et =~ s/\r?\n$//s; + #printf(STDERR "%s %15.15s.%-5hu %15.15s.%-5hu %2hu %3u %u\n", + # $et, + # $Cflow::srcip, $Cflow::srcport, + # $Cflow::dstip, $Cflow::dstport, + # $Cflow::protocol, $Cflow::bytes); + + # 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 (grep { $_ !~ m/^-/ } keys(%{$host->{-target}})) { + my $matched = 0; + my $inbound; $inbound = undef; + my $np = $ctx->{-network}->{$host->{-name}.":".$target}; + if ($np->match_string($Cflow::srcip)) { $inbound = 0; } + if ($np->match_string($Cflow::dstip)) { $inbound = 1; } + if (defined($inbound)) { + foreach my $service (@{$host->{-target}->{$target}->{-service}}) { + my $services = $cfg->{'Service'}->{$service}; + foreach my $s (@{$services}) { + my $proto = $cfg->{'Protocol'}->{$s->{-proto}}; + my $port = $s->{-port}; + if ($Cflow::protocol == $proto) { + if ( ( $inbound and $Cflow::dstport == $port) + or (not $inbound and $Cflow::srcport == $port)) { + $matched = 1; + } + } + if ($matched) { + # 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'}, + '--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"); + 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; } + } + else { + $t = 0; + } + return $t; + } + 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); + } + 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; + } + $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; + # FIXME: UNKNOWN data? + 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'}, $ds_name)); + push(@def, sprintf("DEF:%s_i=%s:%s_i:LAST", $ds_name, $cfg->{'Database'}, $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 - 0x404040"; + push(@draw_o, sprintf("%s:data%d_o#%06x:%s out", ($i == 0 ? "AREA" : "STACK"), $i, $color_o, $target)); + push(@draw_i, sprintf("%s:data%d_i#%06x:%s in", ($i == 0 ? "AREA" : "STACK"), $i, $color_i, $target)); + $i++; + } + my @draw = (@draw_o, "COMMENT:\n", @draw_i, "HRULE:0#000000"); + 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#ffffff'); + push(@args, '--color', 'BACK#e0e0e0'); + push(@args, '--color', 'SHADEA#f8f8f8'); + push(@args, '--color', 'SHADEB#999999'); + push(@args, '--color', 'GRID#cccccc'); + push(@args, '--color', 'MGRID#000000'); + 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); + print STDERR join(" ", @args)."\n"; + my ($rrd_averages, $rrd_xsize, $rrd_ysize) = RRDs::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; + 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'}, $ds_name)); + push(@def, sprintf("DEF:%s_i=%s:%s_i:LAST", $ds_name, $cfg->{'Database'}, $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 - 0x404040"; + push(@draw_o, sprintf("%s:data%d_o#%06x:%s out", ($i == 0 ? "AREA" : "STACK"), $i, $color_o, $service)); + push(@draw_i, sprintf("%s:data%d_i#%06x:%s in", ($i == 0 ? "AREA" : "STACK"), $i, $color_i, $service)); + $i++; + } + my @draw = (@draw_o, "COMMENT:\n", @draw_i, "HRULE:0#000000"); + 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#ffffff'); + push(@args, '--color', 'BACK#e0e0e0'); + push(@args, '--color', 'SHADEA#f8f8f8'); + push(@args, '--color', 'SHADEB#999999'); + push(@args, '--color', 'GRID#cccccc'); + push(@args, '--color', 'MGRID#000000'); + 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); + print STDERR join(" ", @args)."\n"; + my ($rrd_averages, $rrd_xsize, $rrd_ysize) = RRDs::graph(@args); + my $err = RRDs::error(); + die "failed to generate graph from RRD file: $err" if (defined($err)); + } + } +} + +## +## ==== OPERATION MODE 3: GENERATE WEB USER INTERFACE ==== +## + +if ($opt->{-cgi}) { + my $cgi = new CGI; + + if (defined(my $graph = $cgi->param("graph"))) { + # + # output graph image + # + + # prepare graph generation + my (undef, $tmpfile) = mkstemp(($ENV{'TMPDIR'} || '/tmp') . "/flow2rrd.tmp"); + $graph =~ s|\@|\@$tmpfile|s; + + # generate graph image + my $rc = system("$0 --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) + } + else { + # + # output HTML page + # + + # generate HTML page + my $html = new String::Divert; + $html->overload(1); + $html .= "\n" . + " \n" . + " " . $html->folder("head") . + " \n" . + " \n" . + " " . $html->folder("body") . + " \n" . + "\n"; + $html >> "body"; + $html .= "foo\n"; + $html << 1; + $html->undivert(0); + + # FIXME + + # send out page + print STDOUT $cgi->header( + -type => 'text/html', + -content_length => length($html), + -expires => '+5m' + ) . $html; + + # cleanup + undef $html; + } + + # width + # height + # start-time + # end-time + # upper-limit + # lower-limit +} + +exit(0); +