summaryrefslogtreecommitdiffstats
path: root/lib/Smokeping.pm
diff options
context:
space:
mode:
Diffstat (limited to 'lib/Smokeping.pm')
-rw-r--r--lib/Smokeping.pm2613
1 files changed, 2613 insertions, 0 deletions
diff --git a/lib/Smokeping.pm b/lib/Smokeping.pm
new file mode 100644
index 0000000..9f66792
--- /dev/null
+++ b/lib/Smokeping.pm
@@ -0,0 +1,2613 @@
+# -*- perl -*-
+package Smokeping;
+
+use strict;
+use CGI;
+use Getopt::Long;
+use Pod::Usage;
+use Digest::MD5 qw(md5_base64);
+use SNMP_util;
+use SNMP_Session;
+use POSIX;
+use ISG::ParseConfig;
+use RRDs;
+use Sys::Syslog qw(:DEFAULT setlogsock);
+setlogsock('unix')
+ if grep /^ $^O $/xo, ("linux", "openbsd", "freebsd", "netbsd");
+use File::Basename;
+
+# globale persistent variables for speedy
+use vars qw($cfg $probes $VERSION $havegetaddrinfo $cgimode);
+$VERSION="1.38";
+
+# we want opts everywhere
+my %opt;
+
+BEGIN {
+ $havegetaddrinfo = 0;
+ eval 'use Socket6';
+ $havegetaddrinfo = 1 unless $@;
+}
+
+my $DEFAULTPRIORITY = 'info'; # default syslog priority
+
+my $logging = 0; # keeps track of whether we have a logging method enabled
+
+sub do_log(@);
+sub load_probe($$$$);
+
+sub load_probes ($){
+ my $cfg = shift;
+ my %prbs;
+ foreach my $probe (keys %{$cfg->{Probes}}) {
+ my @subprobes = grep { ref $cfg->{Probes}{$probe}{$_} eq 'HASH' } keys %{$cfg->{Probes}{$probe}};
+ if (@subprobes) {
+ my $modname = $probe;
+ my %properties = %{$cfg->{Probes}{$probe}};
+ delete @properties{@subprobes};
+ for my $subprobe (@subprobes) {
+ for (keys %properties) {
+ $cfg->{Probes}{$probe}{$subprobe}{$_} = $properties{$_}
+ unless exists $cfg->{Probes}{$probe}{$subprobe}{$_};
+ }
+ $prbs{$subprobe} = load_probe($modname, $cfg->{Probes}{$probe}{$subprobe},$cfg, $subprobe);
+ }
+ } else {
+ $prbs{$probe} = load_probe($probe, $cfg->{Probes}{$probe},$cfg, $probe);
+ }
+ }
+ return \%prbs;
+};
+
+sub load_probe ($$$$) {
+ my $modname = shift;
+ my $properties = shift;
+ my $cfg = shift;
+ my $name = shift;
+ $name = $modname unless defined $name;
+ eval 'require probes::'.$modname;
+ die "$@\n" if $@;
+ my $rv;
+ eval '$rv = probes::'.$modname.'->new( $properties,$cfg,$name);';
+ die "$@\n" if $@;
+ die "Failed to load Probe $name (module $modname)\n" unless defined $rv;
+ return $rv;
+}
+
+sub snmpget_ident ($) {
+ my $host = shift;
+ $SNMP_Session::suppress_warnings = 10; # be silent
+ my @get = snmpget("${host}::1:1:1", qw(sysContact sysName sysLocation));
+ return undef unless @get;
+ my $answer = join "/", grep { defined } @get;
+ $answer =~ s/\s+//g;
+ return $answer;
+}
+
+sub lnk ($$) {
+ my ($q, $path) = @_;
+ if ($q->isa('dummyCGI')) {
+ return $path . ".html";
+ } else {
+ return ($q->script_name() || '') . "?target=" . $path;
+ }
+}
+
+sub update_dynaddr ($$){
+ my $cfg = shift;
+ my $q = shift;
+ my @target = split /\./, $q->param('target');
+ my $secret = md5_base64($q->param('secret'));
+ my $address = $ENV{REMOTE_ADDR};
+ my $targetptr = $cfg->{Targets};
+ foreach my $step (@target){
+ return "Error: Unknown Target $step"
+ unless defined $targetptr->{$step};
+ $targetptr = $targetptr->{$step};
+ };
+ return "Error: Invalid Target"
+ unless defined $targetptr->{host} and
+ $targetptr->{host} eq "DYNAMIC/${secret}";
+ my $file = $cfg->{General}{datadir}."/".(join "/", @target);
+ my $prevaddress = "?";
+ my $snmp = snmpget_ident $address;
+ if (-r "$file.adr" and not -z "$file.adr"){
+ open(D, "<$file.adr")
+ or return "Error opening $file.adr: $!\n";
+ chomp($prevaddress = <D>);
+ close D;
+ }
+
+ if ( $prevaddress ne $address){
+ open(D, ">$file.adr.new")
+ or return "Error writing $file.adr.new: $!";
+ print D $address,"\n";
+ close D;
+ rename "$file.adr.new","$file.adr";
+ }
+ if ( $snmp ) {
+ open (D, ">$file.snmp.new")
+ or return "Error writing $file.snmp.new: $!";
+ print D $snmp,"\n";
+ close D;
+ rename "$file.snmp.new", "$file.snmp";
+ } elsif ( -f "$file.snmp") { unlink "$file.snmp" };
+
+}
+sub sendmail ($$$){
+ my $from = shift;
+ my $to = shift;
+ $to = $1 if $to =~ /<(.*?)>/;
+ my $body = shift;
+ if ($cfg->{General}{mailhost}){
+ my $smtp = Net::SMTP->new($cfg->{General}{mailhost});
+ $smtp->mail($from);
+ $smtp->to(split(/\s*,\s*/, $to));
+ $smtp->data();
+ $smtp->datasend($body);
+ $smtp->dataend();
+ $smtp->quit;
+ } elsif ($cfg->{General}{sendmail} or -x "/usr/lib/sendmail"){
+ open (M, "|-") || exec (($cfg->{General}{sendmail} || "/usr/lib/sendmail"),"-f",$from,$to);
+ print M $body;
+ close M;
+ }
+}
+
+sub sendsnpp ($$){
+ my $to = shift;
+ my $msg = shift;
+ if ($cfg->{General}{snpphost}){
+ my $snpp = Net::SNPP->new($cfg->{General}{snpphost}, Timeout => 60);
+ $snpp->send( Pager => $to,
+ Message => $msg) || do_debuglog("ERROR - ". $snpp->message);
+ $snpp->quit;
+ }
+}
+
+sub init_alerts ($){
+ my $cfg = shift;
+ foreach my $al (keys %{$cfg->{Alerts}}) {
+ my $x = $cfg->{Alerts}{$al};
+ next unless ref $x eq 'HASH';
+ if ($x->{type} eq 'matcher'){
+ $x->{pattern} =~ /(\S+)\((.+)\)/
+ or die "ERROR: Alert $al pattern entry '$_' is invalid\n";
+ my $matcher = $1;
+ my $arg = $2;
+ eval 'require matchers::'.$matcher;
+ die "Matcher '$matcher' could not be loaded: $@\n" if $@;
+ my $hand;
+ eval "\$hand = matchers::$matcher->new($arg)";
+ die "ERROR: Matcher '$matcher' could not be instantiated\nwith arguments $arg:\n$@\n" if $@;
+ $x->{length} = $hand->Length;
+ $x->{sub} = sub { $hand->Test(shift) } ;
+ } else {
+ my $sub_front = <<SUB;
+sub {
+ my \$d = shift;
+ my \$y = \$d->{$x->{type}};
+ for(1){
+SUB
+ my $sub;
+ my $sub_back = " return 1;\n }\n return 0;\n}\n";
+ my @ops = split /\s*,\s*/, $x->{pattern};
+ $x->{length} = scalar grep /^[!=><]/, @ops;
+ my $multis = scalar grep /^[*]/, @ops;
+ my $it = "";
+ for(1..$multis){
+ my $ind = " " x ($_-1);
+ $sub .= <<FOR;
+$ind my \$i$_;
+$ind for(\$i$_=0; \$i$_<\$imax$_;\$i$_++){
+FOR
+ };
+ my $i = - $x->{length};
+ my $incr = 0;
+ for (@ops) {
+ my $extra = "";
+ $it = " " x $multis;
+ for(1..$multis){
+ $extra .= "-\$i$_";
+ };
+ /^(==|!=|<|>|<=|>=|\*)(\d+(?:\.\d*)?|U|S|\d*\*)(%?)$/
+ or die "ERROR: Alert $al pattern entry '$_' is invalid\n";
+ my $op = $1;
+ my $value = $2;
+ my $perc = $3;
+ if ($op eq '*') {
+ if ($value =~ /^([1-9]\d*)\*$/) {
+ $value = $1;
+ $x->{length} += $value;
+ $sub_front .= " my \$imax$multis = $value;\n";
+ $sub_back .= "\n";
+ $sub .= <<FOR;
+$it last;
+$it }
+$it return 0 if \$i$multis >= \$imax$multis;
+FOR
+
+ $multis--;
+ next;
+ } else {
+ die "ERROR: multi-match operator * must be followed by Number* in Alert $al definition\n";
+ }
+ } elsif ($value eq 'U') {
+ if ($op eq '==') {
+ $sub .= "$it next if defined \$y->[$i$extra];\n";
+ } elsif ($op eq '!=') {
+ $sub .= "$it next unless defined \$y->[$i$extra];\n";
+ } else {
+ die "ERROR: invalid operator $op in connection U in Alert $al definition\n";
+ }
+ } elsif ($value eq 'S') {
+ if ($op eq '==') {
+ $sub .= "$it next unless defined \$y->[$i$extra] and \$y->[$i$extra] eq 'S';\n";
+ } else {
+ die "ERROR: S is only valid with == operator in Alert $al definition\n";
+ }
+ } elsif ($value eq '*') {
+ if ($op ne '==') {
+ die "ERROR: operator $op makes no sense with * in Alert $al definition\n";
+ } # do nothing else ...
+ } else {
+ if ( $x->{type} eq 'loss') {
+ die "ERROR: loss should be specified in % (alert $al pattern)\n" unless $perc eq "%";
+ } elsif ( $x->{type} eq 'rtt' ) {
+ $value /= 1000;
+ } else {
+ die "ERROR: unknown alert type $x->{type}\n";
+ }
+ $sub .= <<IF;
+$it next unless defined \$y->[$i$extra]
+$it and \$y->[$i$extra] =~ /^\\d/
+$it and \$y->[$i$extra] $op $value;
+IF
+ }
+ $i++;
+ }
+ $sub_front .= "$it next if scalar \@\$y < $x->{length} ;\n";
+ do_debuglog(<<COMP);
+### Compiling alert detector pattern '$al'
+### $x->{pattern}
+$sub_front$sub$sub_back
+COMP
+ $x->{sub} = eval ( $sub_front.$sub.$sub_back );
+ die "ERROR: compiling alert pattern $al ($x->{pattern}): $@\n" if $@;
+ }
+ }
+}
+
+
+sub check_filter ($$) {
+ my $cfg = shift;
+ my $name = shift;
+ # remove the path prefix when filtering and make sure the path again starts with /
+ my $prefix = $cfg->{General}{datadir};
+ $name =~ s|^${prefix}/*|/|;
+ # if there is a filter do neither schedule these nor make rrds
+ if ($opt{filter} && scalar @{$opt{filter}}){
+ my $ok = 0;
+ for (@{$opt{filter}}){
+ /^\!(.+)$/ && do {
+ my $rx = $1;
+ $name !~ /^$rx/ && do{ $ok = 1};
+ next;
+ };
+ /^(.+)$/ && do {
+ my $rx = $1;
+ $name =~ /^$rx/ && do {$ok = 1};
+ next;
+ };
+ }
+ return $ok;
+ };
+ return 1;
+}
+
+sub init_target_tree ($$$$$$$$); # predeclare recursive subs
+sub init_target_tree ($$$$$$$$) {
+ my $cfg = shift;
+ my $probes = shift;
+ my $probe = shift;
+ my $tree = shift;
+ my $name = shift;
+ my $PROBE_CONF = shift;
+ my $alerts = shift;
+ my $alertee = shift;
+
+ # inherit probe type from parent
+ if (not defined $tree->{probe} or $tree->{probe} eq $probe){
+ $tree->{probe} = $probe;
+ # inherit parent values if the probe type has not changed
+ for (keys %$PROBE_CONF) {
+ $tree->{PROBE_CONF}{$_} = $PROBE_CONF->{$_}
+ unless exists $tree->{PROBE_CONF}{$_};
+ }
+ };
+
+ $tree->{alerts} = $alerts
+ if not defined $tree->{alerts} and defined $alerts;
+
+ $tree->{alertee} = $alertee
+ if not defined $tree->{alertee} and defined $alertee;
+
+ if ($tree->{alerts}){
+ die "ERROR: no Alerts section\n"
+ unless exists $cfg->{Alerts};
+ $tree->{alerts} = [ split(/\s*,\s*/, $tree->{alerts}) ] unless ref $tree->{alerts} eq 'ARRAY';
+ $tree->{fetchlength} = 0;
+ foreach my $al (@{$tree->{alerts}}) {
+ die "ERROR: alert $al ($name) is not defined\n"
+ unless defined $cfg->{Alerts}{$al};
+ $tree->{fetchlength} = $cfg->{Alerts}{$al}{length}
+ if $tree->{fetchlength} < $cfg->{Alerts}{$al}{length};
+ }
+ };
+ # fill in menu and title if missing
+ $tree->{menu} ||= $tree->{host} || "unknown";
+ $tree->{title} ||= $tree->{host} || "unknown";
+
+ foreach my $prop (keys %{$tree}) {
+ next if $prop eq 'PROBE_CONF';
+ if (ref $tree->{$prop} eq 'HASH'){
+ if (not -d $name) {
+ mkdir $name, 0755 or die "ERROR: mkdir $name: $!\n";
+ };
+ init_target_tree $cfg, $probes, $tree->{probe}, $tree->{$prop}, "$name/$prop", $tree->{PROBE_CONF},$tree->{alerts},$tree->{alertee};
+ }
+ if ($prop eq 'host' and check_filter($cfg,$name)) {
+ # print "init $name\n";
+ die "Error: Invalid Probe: $tree->{probe}" unless defined $probes->{$tree->{probe}};
+ my $probeobj = $probes->{$tree->{probe}};
+ my $step = $probeobj->step();
+ # we have to do the add before calling the _pings method, it won't work otherwise
+ if($tree->{$prop} =~ /^DYNAMIC/) {
+ $probeobj->add($tree,$name);
+ } else {
+ $probeobj->add($tree,$tree->{$prop});
+ }
+ my $pings = $probeobj->_pings($tree);
+
+ if (not -f $name.".rrd"){
+ my @create =
+ ($name.".rrd", "--step",$step,
+ "DS:uptime:GAUGE:".(2*$step).":0:U",
+ "DS:loss:GAUGE:".(2*$step).":0:".$pings,
+ # 180 Seconds is the max rtt we consider valid ...
+ "DS:median:GAUGE:".(2*$step).":0:180",
+ (map { "DS:ping${_}:GAUGE:".(2*$step).":0:180" }
+ 1..$pings),
+ (map { "RRA:".(join ":", @{$_}) } @{$cfg->{Database}{_table}} ));
+ do_debuglog("Calling RRDs::create(@create)");
+ RRDs::create(@create);
+ my $ERROR = RRDs::error();
+ do_log "RRDs::create ERROR: $ERROR\n" if $ERROR;
+ }
+ }
+ }
+};
+
+sub enable_dynamic($$$$);
+sub enable_dynamic($$$$){
+ my $cfg = shift;
+ my $cfgfile = $cfg->{__cfgfile};
+ my $tree = shift;
+ my $path = shift;
+ my $email = ($tree->{email} || shift);
+ my $print;
+ die "ERROR: smokemail property in $cfgfile not specified\n" unless defined $cfg->{General}{smokemail};
+ die "ERROR: cgiurl property in $cfgfile not specified\n" unless defined $cfg->{General}{cgiurl};
+ if (defined $tree->{host} and $tree->{host} eq 'DYNAMIC' ) {
+ if ( not defined $email ) {
+ warn "WARNING: No email address defined for $path\n";
+ } else {
+ my $usepath = $path;
+ $usepath =~ s/\.$//;
+ my $secret = int(rand 1000000);
+ my $md5 = md5_base64($secret);
+ open C, "<$cfgfile" or die "ERROR: Reading $cfgfile: $!\n";
+ open G, ">$cfgfile.new" or die "ERROR: Writing $cfgfile.new: $!\n";
+ my $section ;
+ my @goal = split /\./, $usepath;
+ my $indent = "+";
+ my $done;
+ while (<C>){
+ $done && do { print G; next };
+ /^\s*\Q*** Targets ***\E\s*$/ && do{$section = 'match'};
+ @goal && $section && /^\s*\Q${indent}\E\s*\Q$goal[0]\E/ && do {
+ $indent .= "+";
+ shift @goal;
+ };
+ (not @goal) && /^\s*host\s*=\s*DYNAMIC$/ && do {
+ print G "host = DYNAMIC/$md5\n";
+ $done = 1;
+ next;
+ };
+ print G;
+ }
+ close G;
+ rename "$cfgfile.new", $cfgfile;
+ close C;
+ my $body;
+ open SMOKE, $cfg->{General}{smokemail} or die "ERROR: can't read $cfg->{General}{smokemail}: $!\n";
+ while (<SMOKE>){
+ s/<##PATH##>/$usepath/ig;
+ s/<##SECRET##>/$secret/ig;
+ s/<##URL##>/$cfg->{General}{cgiurl}/;
+ s/<##FROM##>/$cfg->{General}{contact}/;
+ s/<##OWNER##>/$cfg->{General}{owner}/;
+ s/<##TO##>/$email/;
+ $body .= $_;
+ }
+ close SMOKE;
+
+
+ my $mail;
+ print STDERR "Sending smoke-agent for $usepath to $email ... ";
+ sendmail $cfg->{General}{contact},$email,$body;
+ print STDERR "DONE\n";
+ }
+ }
+ foreach my $prop ( keys %{$tree}) {
+ next if $prop eq "PROBE_CONF";
+ enable_dynamic $cfg, $tree->{$prop},"$path$prop.",$email if ref $tree->{$prop} eq 'HASH';
+ }
+};
+
+
+sub target_menu($$$;$);
+sub target_menu($$$;$){
+ my $tree = shift;
+ my $open = shift;
+ my $path = shift;
+ my $suffix = shift || '';
+ my $print;
+ my $current = shift @{$open} || "";
+
+ my @hashes;
+ foreach my $prop (sort { $tree->{$a}{_order} <=> $tree->{$b}{_order}}
+ grep { ref $tree->{$_} eq 'HASH' and $_ ne "PROBE_CONF" }
+ keys %{$tree}) {
+ push @hashes, $prop;
+ }
+ return "" unless @hashes;
+ $print .= "<table width=\"100%\" class=\"menu\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\">\n";
+ for (@hashes) {
+ my $class;
+ if ($_ eq $current ){
+ if ( @$open ) {
+ $class = 'menuopen';
+ } else {
+ $class = 'menuactive';
+ }
+ } else {
+ $class = 'menuitem';
+ };
+ my $menu = $tree->{$_}{menu};
+ $menu =~ s/ /&nbsp;/g;
+ my $menuadd ="";
+ $menuadd = "&nbsp;" x (20 - length($menu)) if length($menu) < 20;
+ $print .= "<tr><td class=\"$class\" colspan=\"2\">&nbsp;-&nbsp;<a class=\"menulink\" HREF=\"$path$_$suffix\">$menu</a>$menuadd</td></tr>\n";
+ if ($_ eq $current){
+ my $prline = target_menu $tree->{$_}, $open, "$path$_.", $suffix;
+ $print .= "<tr><td class=\"$class\">&nbsp;&nbsp;</td><td align=\"left\">$prline</td></tr>"
+ if $prline;
+ }
+ }
+ $print .= "</table>\n";
+ return $print;
+};
+
+
+
+sub fill_template ($$){
+ my $template = shift;
+ my $subst = shift;
+ my $line = $/;
+ undef $/;
+ open I, $template or return "<HTML><BODY>ERROR: Reading page template $template: $!</BODY></HTML>";
+ my $data = <I>;
+ close I;
+ $/ = $line;
+ foreach my $tag (keys %{$subst}) {
+ $data =~ s/<##${tag}##>/$subst->{$tag}/g;
+ }
+ return $data;
+}
+
+sub exp2seconds ($) {
+ my $x = shift;
+ $x =~/(\d+)m/ && return $1*60;
+ $x =~/(\d+)h/ && return $1*60*60;
+ $x =~/(\d+)d/ && return $1*60*60*24;
+ $x =~/(\d+)w/ && return $1*60*60*24*7;
+ $x =~/(\d+)y/ && return $1*60*60*24*365;
+ return $x;
+}
+
+sub get_overview ($$$$){
+ my $cfg = shift;
+ my $q = shift;
+ my $tree = shift;
+ my $open = shift;
+ my $dir = "";
+
+ my $page ="";
+
+ for (@$open) {
+ $dir .= "/$_";
+ mkdir $cfg->{General}{imgcache}.$dir, 0755
+ unless -d $cfg->{General}{imgcache}.$dir;
+ die "ERROR: creating $cfg->{General}{imgcache}$dir: $!\n"
+ unless -d $cfg->{General}{imgcache}.$dir;
+ }
+ my $date = $cfg->{Presentation}{overview}{strftime} ?
+ POSIX::strftime($cfg->{Presentation}{overview}{strftime},
+ localtime(time)) : scalar localtime(time);
+ foreach my $prop (sort {$tree->{$a}{_order} <=> $tree->{$b}{_order}}
+ grep { ref $tree->{$_} eq 'HASH' and $_ ne "PROBE_CONF" and defined $tree->{$_}{host}}
+ keys %$tree) {
+ my $rrd = $cfg->{General}{datadir}.$dir."/$prop.rrd";
+ my $max = $cfg->{Presentation}{overview}{max_rtt} || "100000";
+ my $medc = $cfg->{Presentation}{overview}{median_color} || "ff0000";
+ my $probe = $probes->{$tree->{$prop}{probe}};
+ my $pings = $probe->_pings($tree->{$prop});
+ my ($graphret,$xs,$ys) = RRDs::graph
+ ($cfg->{General}{imgcache}.$dir."/${prop}_mini.png",
+ '--lazy',
+ '--start','-'.exp2seconds($cfg->{Presentation}{overview}{range}),
+ '--title',$tree->{$prop}{title},
+ '--height',$cfg->{Presentation}{overview}{height},
+ '--width',,$cfg->{Presentation}{overview}{width},
+ '--vertical-label',"Seconds",
+ '--imgformat','PNG',
+ '--lower-limit','0',
+ "DEF:median=${rrd}:median:AVERAGE",
+ "DEF:loss=${rrd}:loss:AVERAGE",
+ "CDEF:ploss=loss,$pings,/,100,*",
+ "CDEF:dm=median,0,$max,LIMIT",
+ "CDEF:dm2=median,1.5,*,0,$max,LIMIT",
+ "LINE1:dm2", # this is for kicking things down a bit
+ "LINE1:dm#$medc:median RTT avg\\: ",
+ "GPRINT:median:AVERAGE: %0.2lf %ss ",
+ "GPRINT:median:LAST: latest RTT\\: %0.2lf %ss ",
+ "GPRINT:ploss:AVERAGE: avg pkg loss\\: %.2lf %% ",
+ "COMMENT: $date\\j");
+ my $ERROR = RRDs::error();
+ $page .= "<div>";
+ if (defined $ERROR) {
+ $page .= "ERROR: $ERROR";
+ } else {
+ $page.="<A HREF=\"".lnk($q, (join ".", @$open, ${prop}))."\">".
+ "<IMG BORDER=\"0\" WIDTH=\"$xs\" HEIGHT=\"$ys\" ".
+ "SRC=\"".$cfg->{General}{imgurl}.$dir."/${prop}_mini.png\"></A>";
+ }
+ $page .="</div>"
+ }
+ return $page;
+}
+
+sub findmax ($$) {
+ my $cfg = shift;
+ my $rrd = shift;
+# my $pings = "ping".int($cfg->{Database}{pings}/1.1);
+ my %maxmedian;
+ my @maxmedian;
+ for (@{$cfg->{Presentation}{detail}{_table}}) {
+ my ($desc,$start) = @{$_};
+ $start = exp2seconds($start);
+ my ($graphret,$xs,$ys) = RRDs::graph
+ ("dummy", '--start', -$start,
+ "DEF:maxping=${rrd}:median:AVERAGE",
+ 'PRINT:maxping:MAX:%le' );
+ my $ERROR = RRDs::error();
+ do_log $ERROR if $ERROR;
+ my $val = $graphret->[0];
+ $val = 1 if $val =~ /nan/i;
+ $maxmedian{$start} = $val;
+ push @maxmedian, $val;
+ }
+ my $med = (sort @maxmedian)[int(($#maxmedian) / 2 )];
+ my $max = 0.000001;
+ foreach my $x ( keys %maxmedian ){
+ if ( not defined $cfg->{Presentation}{detail}{unison_tolerance} or (
+ $maxmedian{$x} <= $cfg->{Presentation}{detail}{unison_tolerance} * $med
+ and $maxmedian{$x} >= $med / $cfg->{Presentation}{detail}{unison_tolerance}) ){
+ $max = $maxmedian{$x} unless $maxmedian{$x} < $max;
+ $maxmedian{$x} = undef;
+ };
+ }
+ foreach my $x ( keys %maxmedian ){
+ if (defined $maxmedian{$x}) {
+ $maxmedian{$x} *= 1.5;
+ } else {
+ $maxmedian{$x} = $max * 1.5;
+ }
+
+ $maxmedian{$x} = $cfg->{Presentation}{detail}{max_rtt}
+ if $cfg->{Presentation}{detail}{max_rtt} and
+ $maxmedian{$x} > $cfg->{Presentation}{detail}{max_rtt}
+ };
+ return \%maxmedian;
+}
+
+sub smokecol ($) {
+ my $count = ( shift )- 2 ;
+ return [] unless $count > 0;
+ my $half = $count/2;
+ my @items;
+ for (my $i=$count; $i > $half; $i--){
+ my $color = int(190/$half * ($i-$half))+50;
+ push @items, "AREA:cp".($i+2)."#".(sprintf("%02x",$color) x 3);
+ };
+ for (my $i=int($half); $i >= 0; $i--){
+ my $color = int(190/$half * ($half - $i))+64;
+ push @items, "AREA:cp".($i+2)."#".(sprintf("%02x",$color) x 3);
+ };
+ return \@items;
+}
+
+sub get_detail ($$$$){
+ my $cfg = shift;
+ my $q = shift;
+ my $tree = shift;
+ my $open = shift;
+ return "" unless $tree->{host};
+ my @dirs = @{$open};
+ my $file = pop @dirs;
+ my $dir = "";
+ die "ERROR: ".(join ".", @dirs)." has no probe defined\n"
+ unless $tree->{probe};
+ die "ERROR: ".(join ".", @dirs)." $tree->{probe} is not known\n"
+ unless $cfg->{__probes}{$tree->{probe}};
+ my $probe = $cfg->{__probes}{$tree->{probe}};
+ my $ProbeDesc = $probe->ProbeDesc();
+ my $step = $probe->step();
+ my $pings = $probe->_pings($tree);
+
+ my $page;
+
+
+ for (@dirs) {
+ $dir .= "/$_";
+ mkdir $cfg->{General}{imgcache}.$dir, 0755
+ unless -d $cfg->{General}{imgcache}.$dir;
+ die "ERROR: creating $cfg->{General}{imgcache}$dir: $!\n"
+ unless -d $cfg->{General}{imgcache}.$dir;
+
+ }
+ my $rrd = $cfg->{General}{datadir}."/".(join "/", @dirs)."/${file}.rrd";
+ my $img = $cfg->{General}{imgcache}."/".(join "/", @dirs)."/${file}.rrd";
+
+ my %lasthight;
+ if (open (HG,"<${img}.maxhight")){
+ while (<HG>){
+ chomp;
+ my @l = split / /;
+ $lasthight{$l[0]} = $l[1];
+ }
+ close HG;
+ }
+ my $max = findmax $cfg, $rrd;
+ if (open (HG,">${img}.maxhight")){
+ foreach my $s (keys %{$max}){
+ print HG "$s $max->{$s}\n";
+ }
+ close HG;
+ }
+
+ my $smoke = $pings - 3 > 0
+ ? smokecol $pings : [ 'COMMENT:"Not enough data collected to draw graph"' ];
+ my @upargs;
+ my @upsmoke;
+ my @median;
+ my $date = $cfg->{Presentation}{detail}{strftime} ?
+ POSIX::strftime($cfg->{Presentation}{detail}{strftime},
+ localtime(time)) : scalar localtime(time);
+
+ for (@{$cfg->{Presentation}{detail}{_table}}) {
+ my ($desc,$start) = @{$_};
+ $start = exp2seconds($start);
+ do {
+ @median = ("DEF:median=${rrd}:median:AVERAGE",
+ "DEF:loss=${rrd}:loss:AVERAGE",
+ "CDEF:ploss=loss,$pings,/,100,*",
+ "GPRINT:median:AVERAGE:Median Ping RTT (avg %.1lf %ss) ",
+ "LINE1:median#202020"
+ );
+ my $p = $pings;
+
+ my %lc;
+ my $lastup = 0;
+ if ( defined $cfg->{Presentation}{detail}{loss_colors}{_table} ) {
+ for (@{$cfg->{Presentation}{detail}{loss_colors}{_table}}) {
+ my ($num,$col,$txt) = @{$_};
+ $lc{$num} = [ $txt, "#".$col ];
+ }
+ } else {
+ %lc = (0 => ['0', '#26ff00'],
+ 1 => ["1/$p", '#00b8ff'],
+ 2 => ["2/$p", '#0059ff'],
+ 3 => ["3/$p", '#5e00ff'],
+ 4 => ["4/$p", '#7e00ff'],
+ int($p/2) => [int($p/2)."/$p", '#dd00ff'],
+ $p-1 => [($p-1)."/$p", '#ff0000'],
+ );
+ };
+ my $last = -1;
+ my $swidth = $max->{$start} / $cfg->{Presentation}{detail}{height};
+ foreach my $loss (sort {$a <=> $b} keys %lc){
+ my $lvar = $loss; $lvar =~ s/\./d/g ;
+ push @median,
+ (
+ "CDEF:me$lvar=loss,$last,GT,loss,$loss,LE,*,1,UNKN,IF,median,*",
+ "CDEF:meL$lvar=me$lvar,$swidth,-",
+ "CDEF:meH$lvar=me$lvar,0,*,$swidth,2,*,+",
+ "AREA:meL$lvar",
+ "STACK:meH$lvar$lc{$loss}[1]:$lc{$loss}[0]"
+ );
+ $last = $loss;
+ }
+ push @median, ( "GPRINT:ploss:AVERAGE: avg pkg loss\\: %.2lf %%\\l" );
+# map {print "$_<br/>"} @median;
+ };
+ # if we have uptime draw a colorful background or the graph showing the uptime
+ my $cdir=$cfg->{General}{datadir}."/".(join "/", @dirs)."/";
+ if (-f "$cdir/${file}.adr") {
+ @upsmoke = ();
+ @upargs = ('COMMENT:Link Up: ',
+ "DEF:uptime=${rrd}:uptime:AVERAGE",
+ "CDEF:duptime=uptime,86400,/",
+ 'GPRINT:duptime:LAST: %0.1lf days (');
+ my %upt;
+ if ( defined $cfg->{Presentation}{detail}{uptime_colors}{_table} ) {
+ for (@{$cfg->{Presentation}{detail}{uptime_colors}{_table}}) {
+ my ($num,$col,$txt) = @{$_};
+ $upt{$num} = [ $txt, "#".$col];
+ }
+ } else {
+ %upt = ( 3600 => ['<1h', '#FFD3D3'],
+ 2*3600 => ['<2h', '#FFE4C7'],
+ 6*3600 => ['<6h', '#FFF9BA'],
+ 12*3600 => ['<12h','#F3FFC0'],
+ 24*3600 => ['<1d', '#E1FFCC'],
+ 7*24*3600 => ['<1w', '#BBFFCB'],
+ 30*24*3600 => ['<1m', '#BAFFF5'],
+ '1e100' => ['>1m', '#DAECFF']
+ );
+ }
+ my $lastup = 0;
+ foreach my $uptime (sort {$a <=> $b} keys %upt){
+ push @upargs,
+ (
+ "CDEF:up$uptime=uptime,$lastup,GE,uptime,$uptime,LE,*,INF,UNKN,IF",
+ "AREA:up$uptime$upt{$uptime}[1]:$upt{$uptime}[0]"
+ );
+ push @upsmoke,
+ (
+ "CDEF:ups$uptime=uptime,$lastup,GE,uptime,$uptime,LE,*,cp2,UNKN,IF",
+ "AREA:ups$uptime$upt{$uptime}[1]"
+ );
+ $lastup=$uptime;
+ }
+
+ push @upargs, 'COMMENT:)\l';
+# map {print "$_<br/>"} @upargs;
+ };
+ my @log = ();
+ push @log, "--logarithmic" if $cfg->{Presentation}{detail}{logarithmic} and
+ $cfg->{Presentation}{detail}{logarithmic} eq 'yes';
+
+ my @lazy =();
+ @lazy = ('--lazy') if $lasthight{$start} and $lasthight{$start} == $max->{$start};
+ my ($graphret,$xs,$ys) = RRDs::graph
+ ($cfg->{General}{imgcache}.$dir."/${file}_last_${start}.png",
+ @lazy,
+ '--start','-'.$start,
+ '--height',$cfg->{Presentation}{detail}{height},
+ '--width',,$cfg->{Presentation}{detail}{width},
+ '--title',$desc,
+ '--rigid',
+ '--upper-limit', $max->{$start},
+ @log,
+ '--lower-limit',(@log ? ($max->{$start} > 0.01) ? '0.001' : '0.0001' : '0'),
+ '--vertical-label',"Seconds",
+ '--imgformat','PNG',
+ '--color', 'SHADEA#ffffff',
+ '--color', 'SHADEB#ffffff',
+ '--color', 'BACK#ffffff',
+ '--color', 'CANVAS#ffffff',
+ (map {"DEF:ping${_}=${rrd}:ping${_}:AVERAGE"} 1..$pings),
+ (map {"CDEF:cp${_}=ping${_},0,$max->{$start},LIMIT"} 1..$pings),
+ @upargs,# draw the uptime bg color
+ @$smoke,
+ @upsmoke, # draw the rest of the uptime bg color
+ @median,
+# 'LINE3:median#ff0000:Median RTT in grey '.$cfg->{Database}{pings}.' pings sorted by RTT',
+# 'LINE1:median#ff8080',
+ # Gray background for times when no data was collected, so they can
+ # be distinguished from network being down.
+ ( $cfg->{Presentation}{detail}{nodata_color} ? (
+ 'CDEF:nodata=loss,UN,INF,UNKN,IF',
+ "AREA:nodata#$cfg->{Presentation}{detail}{nodata_color}" ):
+ ()),
+ 'HRULE:0#000000',
+ 'COMMENT:\s',
+ "COMMENT:Probe: $pings $ProbeDesc every $step seconds",
+ 'COMMENT:created on '.$date.'\j' );
+
+ my $ERROR = RRDs::error();
+ $page .= "<div>".
+ ( $ERROR ||
+ "<IMG BORDER=\"0\" WIDTH=\"$xs\" HEIGHT=\"$ys\" ".
+ "SRC=\"".$cfg->{General}{imgurl}.$dir."/${file}_last_${start}.png\">" )."</div>";
+
+ }
+ return $page;
+}
+
+sub display_webpage($$){
+ my $cfg = shift;
+ my $q = shift;
+ my $open = [ split /\./,( $q->param('target') || '')];
+ my $tree = $cfg->{Targets};
+ my $step = $cfg->{__probes}{$tree->{probe}}->step();
+ for (@$open) {
+ die "ERROR: Section '$_' does not exist.\n"
+ unless exists $tree->{$_};
+ last unless ref $tree->{$_} eq 'HASH';
+ $tree = $tree->{$_};
+ }
+ gen_imgs($cfg); # create logos in imgcache
+
+ print fill_template
+ ($cfg->{Presentation}{template},
+ {
+ menu => target_menu($cfg->{Targets},
+ [@$open], #copy this because it gets changed
+ ($q->script_name() || '')."?target="),
+ title => $tree->{title},
+ remark => ($tree->{remark} || ''),
+ overview => get_overview( $cfg,$q,$tree,$open ),
+ body => get_detail( $cfg,$q,$tree,$open ),
+ target_ip => ($tree->{host} || ''),
+ owner => $cfg->{General}{owner},
+ contact => $cfg->{General}{contact},
+ author => '<A HREF="http://tobi.oetiker.ch/">Tobi&nbsp;Oetiker</A>',
+ smokeping => '<A HREF="http://people.ee.ethz.ch/~oetiker/webtools/smokeping/counter.cgi/'.$VERSION.'">SmokePing-'.$VERSION.'</A>',
+ step => $step,
+ rrdlogo => '<A HREF="http://people.ee.ethz.ch/~oetiker/webtools/rrdtool/"><img border="0" src="'.$cfg->{General}{imgurl}.'/rrdtool.png"></a>',
+ smokelogo => '<A HREF="http://people.ee.ethz.ch/~oetiker/webtools/smokeping/counter.cgi/'.$VERSION.'"><img border="0" src="'.$cfg->{General}{imgurl}.'/smokeping.png"></a>',
+ }
+ );
+}
+
+# fetch all data.
+sub run_probes($$) {
+ my $probes = shift;
+ my $justthisprobe = shift;
+ if (defined $justthisprobe) {
+ $probes->{$justthisprobe}->ping();
+ } else {
+ foreach my $probe (keys %{$probes}) {
+ $probes->{$probe}->ping();
+ }
+ }
+}
+
+# report probe status
+sub report_probes($$) {
+ my $probes = shift;
+ my $justthisprobe = shift;
+ if (defined $justthisprobe) {
+ $probes->{$justthisprobe}->report();
+ } else {
+ foreach my $probe (keys %{$probes}){
+ $probes->{$probe}->report();
+ }
+ }
+}
+
+sub update_rrds($$$$$$);
+sub update_rrds($$$$$$) {
+ my $cfg = shift;
+ my $probes = shift;
+ my $probe = shift;
+ my $tree = shift;
+ my $name = shift;
+ my $justthisprobe = shift; # if defined, update only the targets probed by this probe
+
+ $probe = $tree->{probe} if defined $tree->{probe};
+ my $probeobj = $probes->{$probe};
+ foreach my $prop (keys %{$tree}) {
+
+ next if $prop eq "PROBE_CONF";
+ if (ref $tree->{$prop} eq 'HASH'){
+ update_rrds $cfg, $probes, $probe, $tree->{$prop}, $name."/$prop", $justthisprobe;
+ }
+ next if defined $justthisprobe and $probe ne $justthisprobe;
+ if ($prop eq 'host' and check_filter($cfg,$name)) {
+ #print "update $name\n";
+ my $updatestring = $probeobj->rrdupdate_string($tree);
+ my $pings = $probeobj->_pings($tree);
+ if ( $tree->{rawlog} ){
+ my $file = POSIX::strftime $tree->{rawlog},localtime(time);
+ if (open LOG,">>$name.$file.csv"){
+ print LOG time,"\t",join("\t",split /:/,$updatestring),"\n";
+ close LOG;
+ } else {
+ do_log "Warning: failed to open $file for logging: $!\n";
+ }
+ }
+ my @update = ( $name.".rrd",
+ '--template',(join ":", "uptime", "loss", "median",
+ map { "ping${_}" } 1..$pings),
+ "N:".$updatestring
+ );
+ do_debuglog("Calling RRDs::update(@update)");
+ RRDs::update ( @update );
+ my $ERROR = RRDs::error();
+ do_log "RRDs::update ERROR: $ERROR\n" if $ERROR;
+ # check alerts
+ # disabled
+ if ( $tree->{alerts} ) {
+ $tree->{stack} = {loss=>['S'],rtt=>['S']} unless defined $tree->{stack};
+ my $x = $tree->{stack};
+ my ($loss,$rtt) =
+ (split /:/, $probeobj->rrdupdate_string($tree))[1,2];
+ $loss = undef if $loss eq 'U';
+ my $lossprct = $loss * 100 / $pings;
+ $rtt = undef if $rtt eq 'U';
+ push @{$x->{loss}}, $lossprct;
+ push @{$x->{rtt}}, $rtt;
+ if (scalar @{$x->{loss}} > $tree->{fetchlength}){
+ shift @{$x->{loss}};
+ shift @{$x->{rtt}};
+ }
+ for (@{$tree->{alerts}}) {
+ if ( not $cfg->{Alerts}{$_} ) {
+ do_log "WARNING: Empty alert in ".(join ",", @{$tree->{alerts}})." ($name)\n";
+ next;
+ };
+ if ( ref $cfg->{Alerts}{$_}{sub} ne 'CODE' ) {
+ do_log "WARNING: Alert '$_' did not resolve to a Sub Ref. Skipping\n";
+ next;
+ };
+ if ( &{$cfg->{Alerts}{$_}{sub}}($x) ){
+ # we got a match
+ my $from;
+ my $line = "$name/$prop";
+ my $base = $cfg->{General}{datadir};
+ $line =~ s|^$base/||;
+ $line =~ s|/host$||;
+ $line =~ s|/|.|g;
+ do_log("Alert $_ triggered for $line");
+ my $urlline = $line;
+ $urlline = $cfg->{General}{cgiurl}."?target=".$line;
+ my $loss = "loss: ".join ", ",map {defined $_ ? (/^\d/ ? sprintf "%.0f%%", $_ :$_):"U" } @{$x->{loss}};
+ my $rtt = "rtt: ".join ", ",map {defined $_ ? (/^\d/ ? sprintf "%.0fms", $_*1000 :$_):"U" } @{$x->{rtt}};
+ my $stamp = scalar localtime time;
+ my @to;
+ foreach my $addr (map {$_ ? (split /\s*,\s*/,$_) : ()} $cfg->{Alerts}{to},$tree->{alertee},$cfg->{Alerts}{$_}{to}){
+ next unless $addr;
+ if ( $addr =~ /^\|(.+)/) {
+ system $1,$_,$line,$loss,$rtt,$tree->{host};
+ } elsif ( $addr =~ /^snpp:(.+)/ ) {
+ sendsnpp $1, <<SNPPALERT;
+$cfg->{Alerts}{$_}{comment}
+$_ on $line
+$loss
+$rtt
+SNPPALERT
+ } else {
+ push @to, $addr;
+ }
+ };
+ if (@to){
+ my $to = join ",",@to;
+ sendmail $cfg->{Alerts}{from},$to, <<ALERT;
+To: $to
+From: $cfg->{Alerts}{from}
+Subject: [SmokeAlert] $_ on $line
+
+$stamp
+
+Got a match for alert "$_" for $urlline
+
+Pattern
+-------
+$cfg->{Alerts}{$_}{pattern}
+
+Data (old --> now)
+------------------
+$loss
+$rtt
+
+Comment
+-------
+$cfg->{Alerts}{$_}{comment}
+
+
+
+ALERT
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+sub get_parser () {
+ my $KEY_RE = '[-_0-9a-zA-Z]+';
+ my $KEYD_RE = '[-_0-9a-zA-Z.]+';
+ my $TARGET =
+ {
+ _sections => [ ( "PROBE_CONF", "/$KEY_RE/" ) ],
+ _vars => [ qw (probe menu title alerts note email host remark rawlog alertee) ],
+ _order => 1,
+ _doc => <<DOC,
+Each target section can contain information about a host to monitor as
+well as further target sections. Most variables have already been
+described above. The expression above defines legal names for target
+sections.
+DOC
+ alerts => {
+ _doc => 'Comma separated list of alert names',
+ _re => '([^\s,]+(,[^\s,]+)*)?',
+ _re_error => 'Comma separated list of alert names',
+ },
+ host =>
+ {
+ _doc => <<DOC,
+Can either contain the name of a target host or the string B<DYNAMIC>.
+
+In the second case, the target machine has a dynamic IP address and
+thus is required to regularly contact the SmokePing server to verify
+its IP address. When starting SmokePing with the commandline argument
+B<--email> it will add a secret password to each of the B<DYNAMIC>
+host lines and send a script to the owner of each host. This script
+must be started regularly on the host in question to make sure
+SmokePing monitors the right box. If the target machine supports
+SNMP SmokePing will also query the hosts
+sysContact, sysName and sysLocation properties to make sure it is
+still the same host.
+DOC
+
+ _sub => sub {
+ for ( shift ) {
+ m|^DYNAMIC| && return undef;
+ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/ && return undef;
+ /^[0-9a-f]{0,4}(\:[0-9a-f]{0,4}){0,6}\:[0-9a-f]{0,4}$/i && return undef;
+ my $addressfound = 0;
+ my @tried;
+ if ($havegetaddrinfo) {
+ my @ai;
+ @ai = getaddrinfo( $_, "" );
+ unless ($addressfound = scalar(@ai) > 5) {
+ do_debuglog("WARNING: Hostname '$_' does currently not resolve to an IPv6 address\n");
+ @tried = qw{IPv6};
+ }
+ }
+ unless ($addressfound) {
+ unless ($addressfound = gethostbyname( $_ )) {
+ do_debuglog("WARNING: Hostname '$_' does currently not resolve to an IPv4 address\n");
+ push @tried, qw{IPv4};
+ }
+ }
+ unless ($addressfound) {
+ # do not bomb, as this could be temporary
+ my $tried = join " or ", @tried;
+ warn "WARNING: Hostname '$_' does currently not resolve to an $tried address\n" unless $cgimode;
+ }
+ return undef;
+ }
+ return undef;
+ },
+ },
+ email => { _re => '.+\s<\S+@\S+>',
+ _re_error =>
+ "use an email address of the form 'First Last <em\@ail.kg>'",
+ _doc => <<DOC,
+This is the contact address for the owner of the current host. In connection with the B<DYNAMIC> hosts,
+the address will be used for sending the belowmentioned script.
+DOC
+ },
+ note => { _doc => <<DOC },
+Some information about this entry which does NOT get displayed on the web.
+DOC
+ rawlog => { _doc => <<DOC,
+Log the raw data, gathered for this target, in tab separated format, to a file with the
+same basename as the corresponding RRD file. Use posix strftime to format the timestamp to be
+put into the file name. The filename is built like this:
+
+ basename.strftime.csv
+
+Example:
+
+ rawlog=%Y-%m-%d
+
+this would create a new logfile every day with a name like this:
+
+ targethost.2004-05-03.csv
+
+DOC
+ _sub => sub {
+ eval ( "POSIX::strftime('$_[0]', localtime(time))");
+ return $@ if $@;
+ return undef;
+ },
+ },
+ alertee => { _re => '(\|.+|.+@\S+|snpp:)',
+ _re_error => 'the alertee must be an email address here',
+ _doc => <<DOC },
+If you want to have alerts for this target and all targets below it go to a particular address
+on top of the address already specified in the alert, you can add it here. This can be a comma separated list of items.
+DOC
+
+ };
+
+ $TARGET->{ "/$KEY_RE/" } = $TARGET;
+
+ my $PROBEVARS = {
+ _vars => [ "/$KEYD_RE/" ],
+ _doc => <<DOC,
+Probe specific variables.
+DOC
+ "/$KEYD_RE/" => { _doc => <<DOC },
+Should be found in the documentation of the
+corresponding probe. The values get propagated to those child
+nodes using the same Probe.
+DOC
+ };
+
+ $TARGET->{PROBE_CONF} = $PROBEVARS;
+
+ my $INTEGER_SUB = {
+ _sub => sub {
+ return "must be an integer >= 1"
+ unless $_[ 0 ] == int( $_[ 0 ] ) and $_[ 0 ] >= 1;
+ return undef;
+ }
+ };
+ my $DIRCHECK_SUB = {
+ _sub => sub {
+ return "Directory '$_[0]' does not exist" unless -d $_[ 0 ];
+ return undef;
+ }
+ };
+
+ my $FILECHECK_SUB = {
+ _sub => sub {
+ return "File '$_[0]' does not exist" unless -f $_[ 0 ];
+ return undef;
+ }
+ };
+
+ my $PROBES = {
+ _doc => <<DOC,
+Each module can take specific configuration information from this area. The jumble of letters above is a regular expression defining legal module names.
+DOC
+ _vars => [ "step", "offset", "pings", "/$KEYD_RE/" ],
+ "/$KEYD_RE/" => { _doc => 'Each module defines which
+variables it wants to accept. So this expression here just defines legal variable names.'},
+ "step" => { %$INTEGER_SUB,
+ _doc => <<DOC },
+Duration of the base interval that this probe should use, if different
+from the one specified in the 'Database' section. Note that the step in
+the RRD files is fixed when they are originally generated, and if you
+change the step parameter afterwards, you'll have to delete the old RRD
+files or somehow convert them. (This variable is only applicable if
+the variable 'concurrentprobes' is set in the 'General' section.)
+DOC
+ "offset" => {
+ _re => '(\d+%|random)',
+ _re_error =>
+ "Use offset either in % of operation interval or 'random'",
+ _doc => <<DOC },
+If you run many probes concurrently you may want to prevent them from
+hitting your network all at the same time. Using the probe-specific
+offset parameter you can change the point in time when each probe will
+be run. Offset is specified in % of total interval, or alternatively as
+'random', and the offset from the 'General' section is used if nothing
+is specified here. Note that this does NOT influence the rrds itself,
+it is just a matter of when data acqusition is initiated.
+(This variable is only applicable if the variable 'concurrentprobes' is set
+in the 'General' section.)
+DOC
+ "pings" => {
+ %$INTEGER_SUB,
+ _doc => <<DOC},
+How many pings should be sent to each target, if different from the global
+value specified in the Database section. Some probes (those derived from
+basefork.pm, ie. most except the FPing variants) will even let this be
+overridden target-specifically in the PROBE_CONF section (see the
+basefork documentation for details). Note that the number of pings in
+the RRD files is fixed when they are originally generated, and if you
+change this parameter afterwards, you'll have to delete the old RRD
+files or somehow convert them.
+DOC
+ }; # $PROBES
+
+ my $PROBESTOP = {};
+ %$PROBESTOP = %$PROBES;
+ $PROBESTOP->{_sections} = ["/$KEY_RE/"];
+ $PROBESTOP->{"/$KEY_RE/"} = $PROBES;
+ for (qw(step offset pings)) {
+ # we need a deep copy of these
+ my %h = %{$PROBESTOP->{$_}};
+ $PROBES->{$_} = \%h;
+ delete $PROBES->{$_}{_doc}
+ }
+ $PROBES->{_doc} = <<DOC;
+You can define multiple instances of the same probe with subsections.
+These instances can have different values for their variables, so you
+can eg. have one instance of the FPing probe with packet size 1000 and
+step 30 and another instance with packet size 64 and step 300.
+The name of the subsection determines what the probe will be called, so
+you can write descriptive names for the probes.
+
+If there are any subsections defined, the main section for this probe
+will just provide default parameter values for the probe instances, ie.
+it will not become a probe instance itself.
+DOC
+
+ my $parser = ISG::ParseConfig->new
+ (
+ {
+ _sections => [ qw(General Database Presentation Probes Alerts Targets) ],
+ _mandatory => [ qw(General Database Presentation Probes Targets) ],
+ General =>
+ {
+ _doc => <<DOC,
+General configuration values valid for the whole SmokePing setup.
+DOC
+ _vars =>
+ [ qw(owner imgcache imgurl datadir pagedir piddir sendmail offset
+ smokemail cgiurl mailhost contact netsnpp
+ syslogfacility syslogpriority concurrentprobes changeprocessnames) ],
+ _mandatory =>
+ [ qw(owner imgcache imgurl datadir piddir
+ smokemail cgiurl contact) ],
+ imgcache =>
+ { %$DIRCHECK_SUB,
+ _doc => <<DOC,
+A directory which is visible on your webserver where SmokePing can cache graphs.
+DOC
+ },
+
+ imgurl =>
+ {
+ _doc => <<DOC,
+Either an absolute URL to the B<imgcache> directory or one relative to the directory where you keep the
+SmokePing cgi.
+DOC
+ },
+
+ pagedir =>
+ {
+ %$DIRCHECK_SUB,
+ _doc => <<DOC,
+Directory to store static representations of pages.
+DOC
+ },
+ owner =>
+ {
+ _doc => <<DOC,
+Name of the person responsible for this smokeping installation.
+DOC
+ },
+
+ mailhost =>
+ {
+ _doc => <<DOC,
+Instead of using sendmail, you can specify the name of an smtp server
+and use perl's Net::SMTP module to send mail to DYNAMIC host owners (see below).
+DOC
+ _sub => sub { require Net::SMTP ||return "ERROR: loading Net::SMTP"; return undef; }
+ },
+ snpphost =>
+ {
+ _doc => <<DOC,
+If you have a SNPP (Simple Network Pager Protocol) server at hand, you can have alerts
+sent there too. Use the syntax B<snpp:someaddress> to use a snpp address in any place where you can use a mail address otherwhise.
+DOC
+ _sub => sub { require Net::SNPP ||return "ERROR: loading Net::SNPP"; return undef; }
+ },
+
+ contact =>
+ { _re => '\S+@\S+',
+ _re_error =>
+ "use an email address of the form 'name\@place.dom'",
+
+ _doc => <<DOC,
+Mail address of the person responsible for this smokeping installation.
+DOC
+ },
+
+
+ datadir =>
+ {
+ %$DIRCHECK_SUB,
+ _doc => <<DOC,
+The directory where SmokePing can keep its rrd files.
+DOC
+ },
+
+ piddir =>
+ {
+ %$DIRCHECK_SUB,
+ _doc => <<DOC,
+The directory where SmokePing keeps its pid when daemonised.
+DOC
+ },
+ sendmail =>
+ {
+ %$FILECHECK_SUB,
+ _doc => <<DOC,
+Path to your sendmail binary. It will be used for sending mails in connection with the support of DYNAMIC addresses.
+DOC
+ },
+ smokemail =>
+ {
+ %$FILECHECK_SUB,
+ _doc => <<DOC,
+Path to the mail template for DYNAMIC hosts. This mail template
+must contain keywords of the form B<E<lt>##>I<keyword>B<##E<gt>>. There is a sample
+template included with SmokePing.
+DOC
+ },
+ cgiurl =>
+ {
+ _re => 'https?://\S+',
+ _re_error =>
+ "cgiurl must be a http(s)://.... url",
+ _doc => <<DOC,
+Complete URL path of the SmokePing.cgi
+DOC
+
+ },
+ syslogfacility =>
+ {
+ _re => '\w+',
+ _re_error =>
+ "syslogfacility must be alphanumeric",
+ _doc => <<DOC,
+The syslog facility to use, eg. local0...local7.
+Note: syslog logging is only used if you specify this.
+DOC
+ },
+ syslogpriority =>
+ {
+ _re => '\w+',
+ _re_error =>
+ "syslogpriority must be alphanumeric",
+ _doc => <<DOC,
+The syslog priority to use, eg. debug, notice or info.
+Default is $DEFAULTPRIORITY.
+DOC
+ },
+ offset => {
+ _re => '(\d+%|random)',
+ _re_error =>
+ "Use offset either in % of operation interval or 'random'",
+ _doc => <<DOC,
+If you run many instances of smokeping you may want to prevent them from
+hitting your network all at the same time. Using the offset parameter you
+can change the point in time when the probes are run. Offset is specified
+in % of total interval, or alternatively as 'random'. I recommend to use
+'random'. Note that this does NOT influence the rrds itself, it is just a
+matter of when data acqusition is initiated. The default offset is 'random'.
+DOC
+ },
+ concurrentprobes => {
+ _re => '(yes|no)',
+ _re_error =>"this must either be 'yes' or 'no'",
+ _doc => <<DOC,
+If you use multiple probes or multiple instances of the same probe and you
+want them to run concurrently in separate processes, set this to 'yes'. This
+gives you the possibility to specify probe-specific step and offset parameters
+(see the 'Probes' section) for each probe and makes the probes unable to block
+each other in cases of service outages. The default is 'yes', but if you for
+some reason want the old behaviour you can set this to 'no'.
+DOC
+ },
+ changeprocessnames => {
+ _re => '(yes|no)',
+ _re_error =>"this must either be 'yes' or 'no'",
+ _doc => <<DOC,
+When using 'concurrentprobes' (see above), this controls whether the probe
+subprocesses should change their argv string to indicate their probe in
+the process name. If set to 'yes' (the default), the probe name will
+be appended to the process name as '[probe]', eg. '/usr/bin/smokeping
+[FPing]'. If you don't like this behaviour, set this variable to 'no'.
+If 'concurrentprobes' is not set to 'yes', this variable has no effect.
+DOC
+ },
+ },
+ Database =>
+ {
+ _vars => [ qw(step pings) ],
+ _mandatory => [ qw(step pings) ],
+ _doc => <<DOC,
+Describes the properties of the round robin database for storing the
+SmokePing data. Note that it is not possible to edit existing RRDs
+by changing the entries in the cfg file.
+DOC
+
+ step =>
+ { %$INTEGER_SUB,
+ _doc => <<DOC,
+Duration of the base operation interval of SmokePing in seconds.
+SmokePing will venture out every B<step> seconds to ping your target hosts.
+If 'concurrent_probes' is set to 'yes' (see above), this variable can be
+overridden by each probe. Note that the step in the RRD files is fixed when
+they are originally generated, and if you change the step parameter afterwards,
+you'll have to delete the old RRD files or somehow convert them.
+DOC
+ },
+ pings =>
+ {
+ %$INTEGER_SUB,
+ _doc => <<DOC,
+How many pings should be sent to each target. Suggested: 20 pings.
+This can be overridden by each probe. Some probes (those derived from
+basefork.pm, ie. most except the FPing variants) will even let this
+be overridden target-specifically in the PROBE_CONF section (see the
+basefork documentation for details). Note that the number of pings in
+the RRD files is fixed when they are originally generated, and if you
+change this parameter afterwards, you'll have to delete the old RRD
+files or somehow convert them.
+DOC
+ },
+
+ _table =>
+ {
+ _doc => <<DOC,
+This section also contains a table describing the setup of the
+SmokePing database. Below are reasonable defaults. Only change them if
+you know rrdtool and its workings. Each row in the table describes one RRA.
+
+ # cons xff steps rows
+ AVERAGE 0.5 1 1008
+ AVERAGE 0.5 12 4320
+ MIN 0.5 12 4320
+ MAX 0.5 12 4320
+ AVERAGE 0.5 144 720
+ MAX 0.5 144 720
+ MIN 0.5 144 720
+
+DOC
+ _columns => 4,
+ 0 =>
+ {
+ _doc => <<DOC,
+Consolidation method.
+DOC
+ _re => '(AVERAGE|MIN|MAX)',
+ _re_error => "Choose a valid consolidation function",
+ },
+ 1 =>
+ {
+ _doc => <<DOC,
+What part of the consolidated intervals must be known to warrant a known entry.
+DOC
+ _sub => sub {
+ return "Xff must be between 0 and 1"
+ unless $_[ 0 ] > 0 and $_[ 0 ] <= 1;
+ return undef;
+ }
+ },
+ 2 => {%$INTEGER_SUB,
+ _doc => <<DOC,
+How many B<steps> to consolidate into for each RRA entry.
+DOC
+ },
+
+ 3 => {%$INTEGER_SUB,
+ _doc => <<DOC,
+How many B<rows> this RRA should have.
+DOC
+ }
+ }
+ },
+ Presentation =>
+ {
+ _doc => <<DOC,
+Defines how the SmokePing data should be presented.
+DOC
+ _sections => [ qw(overview detail) ],
+ _mandatory => [ qw(overview template detail) ],
+ _vars => [ qw (template charset) ],
+ template =>
+ {
+ _doc => <<DOC,
+The webpage template must contain keywords of the form
+B<E<lt>##>I<keyword>B<##E<gt>>. There is a sample
+template included with SmokePing; use it as the basis for your
+experiments. Default template contains a pointer to the SmokePing
+counter and homepage. I would be glad if you would not remove this as
+it gives me an indication as to how widely used the tool is.
+DOC
+
+ _sub => sub {
+ return "template '$_[0]' not readable" unless -r $_[ 0 ];
+ return undef;
+ }
+ },
+ charset => {
+ _doc => <<DOC,
+By default, SmokePing assumes the 'iso-8859-15' character set. If you use
+something else, this is the place to speak up.
+DOC
+ },
+
+ overview =>
+ { _vars => [ qw(width height range max_rtt median_color strftime) ],
+ _mandatory => [ qw(width height) ],
+ _doc => <<DOC,
+The Overview section defines how the Overview graphs should look.
+DOC
+ max_rtt => { _doc => <<DOC },
+Any roundtrip time larger than this value will cropped in the overview graph
+DOC
+ median_color => { _doc => <<DOC,
+By default the median line is drawn in red. Override it here with a hex color
+in the format I<rrggbb>.
+DOC
+ _re => '[0-9a-f]{6}',
+ _re_error => 'use rrggbb for color',
+ },
+ strftime => { _doc => <<DOC,
+Use posix strftime to format the timestamp in the left hand
+lower corner of the overview graph
+DOC
+ _sub => sub {
+ eval ( "POSIX::strftime( '$_[0]', localtime(time))" );
+ return $@ if $@;
+ return undef;
+ },
+ },
+
+
+ width =>
+ {
+ _sub => sub {
+ return "width must be be an integer >= 10"
+ unless $_[ 0 ] >= 10
+ and int( $_[ 0 ] ) == $_[ 0 ];
+ return undef;
+ },
+ _doc => <<DOC,
+Width of the Overview Graphs.
+DOC
+ },
+ height =>
+ {
+ _doc => <<DOC,
+Height of the Overview Graphs.
+DOC
+ _sub => sub {
+ return "height must be an integer >= 10"
+ unless $_[ 0 ] >= 10
+ and int( $_[ 0 ] ) == $_[ 0 ];
+ return undef;
+ },
+ },
+ range => { _re => '\d+[smhdwy]',
+ _re_error =>
+ "graph range must be a number followed by [smhdwy]",
+ _doc => <<DOC,
+How much time should be depicted in the Overview graph. Time must be specified
+as a number followed by a letter which specifies the unit of time. Known units are:
+B<s>econds, B<m>inutes, B<h>ours, B<d>days, B<w>eeks, B<y>ears.
+DOC
+ },
+ },
+ detail =>
+ {
+ _vars => [ qw(width height logarithmic unison_tolerance max_rtt strftime nodata_color) ],
+ _sections => [ qw(loss_colors uptime_colors) ],
+ _mandatory => [ qw(width height) ],
+ _table => { _columns => 2,
+ _doc => <<DOC,
+The detailed display can contain several graphs of different resolution. In this
+table you can specify the resolution of each graph.
+
+Example:
+
+ "Last 3 Hours" 3h
+ "Last 30 Hours" 30h
+ "Last 10 Days" 10d
+ "Last 400 Days" 400d
+
+DOC
+ 1 =>
+ {
+ _doc => <<DOC,
+How much time should be depicted. The format is the same as for the B<age> parameter of the Overview section.
+DOC
+ _re => '\d+[smhdwy]',
+ _re_error =>
+ "graph age must be a number followed by [smhdwy]",
+ },
+ 0 =>
+ {
+ _doc => <<DOC,
+Description of the particular resolution.
+DOC
+ }
+ },
+ strftime => { _doc => <<DOC,
+Use posix strftime to format the timestamp in the left hand
+lower corner of the detail graph
+DOC
+ _sub => sub {
+ eval ( "
+ POSIX::strftime('$_[0]', localtime(time)) " );
+ return $@ if $@;
+ return undef;
+ },
+ },
+ nodata_color => {
+ _re => '[0-9a-f]{6}',
+ _re_error => "color must be defined with in rrggbb syntax",
+ _doc => "Paint the graph background in a special color when there is no data for this period because smokeping has not been running (#rrggbb)",
+ },
+ logarithmic => { _doc => 'should the graphs be shown in a logarithmic scale (yes/no)',
+ _re => '(yes|no)',
+ _re_error =>"this must either be 'yes' or 'no'",
+ },
+ unison_tolerance => { _doc => "if a graph is more than this factor of the median 'max' it drops out of the unison scaling algorithm. A factor of two would mean that any graph with a max either less than half or more than twice the median 'max' will be dropped from unison scaling",
+ _sub => sub { return "tolerance must be larger than 1" if $_[0] <= 1; return undef},
+ },
+ max_rtt => { _doc => <<DOC },
+Any roundtrip time larger than this value will cropped in the detail graph
+DOC
+ width => { _doc => 'How many pixels wide should detail graphs be',
+ _sub => sub {
+ return "width must be be an integer >= 10"
+ unless $_[ 0 ] >= 10
+ and int( $_[ 0 ] ) == $_[ 0 ];
+ return undef;
+ },
+ },
+ height => { _doc => 'How many pixels high should detail graphs be',
+ _sub => sub {
+ return "height must be an integer >= 10"
+ unless $_[ 0 ] >= 10
+ and int( $_[ 0 ] ) == $_[ 0 ];
+ return undef;
+ },
+ },
+
+ loss_colors => {
+ _table => { _columns => 3,
+ _doc => <<DOC,
+In the Detail view, the color of the median line depends
+the amount of lost packets. SmokePing comes with a reasonable default setting,
+but you may choose to disagree. The table below
+lets you specify your own coloring.
+
+Example:
+
+ Loss Color Legend
+ 1 00ff00 "<1"
+ 3 0000ff "<3"
+ 100 ff0000 ">=3"
+
+DOC
+ 0 =>
+ {
+ _doc => <<DOC,
+Activate when the lossrate (in percent) is larger of equal to this number
+DOC
+ _re => '\d+.?\d*',
+ _re_error =>
+ "I was expecting a number",
+ },
+ 1 =>
+ {
+ _doc => <<DOC,
+Color for this range.
+DOC
+ _re => '[0-9a-f]+',
+ _re_error =>
+ "I was expecting a color of the form rrggbb",
+ },
+
+ 2 =>
+ {
+ _doc => <<DOC,
+Description for this range.
+DOC
+ }
+
+ }, # table
+ }, #loss_colors
+ uptime_colors => {
+ _table => { _columns => 3,
+ _doc => <<DOC,
+When monitoring a host with DYNAMIC addressing, SmokePing will keep
+track of how long the machine is able to keep the same IP
+address. This time is plotted as a color in the graphs
+background. SmokePing comes with a reasonable default setting, but you
+may choose to disagree. The table below lets you specify your own
+coloring
+
+Example:
+
+ # Uptime Color Legend
+ 3600 00ff00 "<1h"
+ 86400 0000ff "<1d"
+ 604800 ff0000 "<1w"
+ 1000000000000 ffff00 ">1w"
+
+Uptime is in days!
+
+DOC
+ 0 =>
+ {
+ _doc => <<DOC,
+Activate when uptime in days is larger of equal to this number
+DOC
+ _re => '\d+.?\d*',
+ _re_error =>
+ "I was expecting a number",
+ },
+ 1 =>
+ {
+ _doc => <<DOC,
+Color for this uptime range range.
+DOC
+ _re => '[0-9a-f]{6}',
+ _re_error =>
+ "I was expecting a color of the form rrggbb",
+ },
+
+ 2 =>
+ {
+ _doc => <<DOC,
+Description for this range.
+DOC
+ }
+
+ },#table
+ }, #uptime_colors
+
+ }, #detail
+ }, #present
+ Probes => { _sections => [ "/$KEY_RE/" ],
+ _doc => <<DOC,
+The Probes Section configures Probe modules. Probe modules integrate an external ping command into SmokePing. Check the documentation of the FPing module for configuration details.
+DOC
+ "/$KEY_RE/" => $PROBESTOP,
+ },
+ Alerts => {
+ _doc => <<DOC,
+The Alert section lets you setup loss and RTT pattern detectors. After each
+round of polling, SmokePing will examine its data and determine which
+detectors match. Detectors are enabled per target and get inherited by
+the targets children.
+
+Detectors are not just simple thresholds which go off at first sight
+of a problem. They are configurable to detect special loss or RTT
+patterns. They let you look at a number of past readings to make a
+more educated decision on what kind of alert should be sent, or if an
+alert should be sent at all.
+
+The patterns are numbers prefixed with an operator indicating the type
+of comparison required for a match.
+
+The following RTT pattern detects if a target's RTT goes from constantly
+below 10ms to constantly 100ms and more:
+
+ old ------------------------------> new
+ <10,<10,<10,<10,<10,>10,>100,>100,>100
+
+Loss patterns work in a similar way, except that the loss is defined as the
+percentage the total number of received packets is of the total number of packets sent.
+
+ old ------------------------------> new
+ ==0%,==0%,==0%,==0%,>20%,>20%,>=20%
+
+Apart from normal numbers, patterns can also contain the values B<*>
+which is true for all values regardless of the operator. And B<U>
+which is true for B<unknown> data together with the B<==> and B<=!> operators.
+
+Detectors normally act on state changes. This has the disadvantage, that
+they will fail to find conditions which were already present when launching
+smokeping. For this it is possible to write detectors that begin with the
+special value B<==S> it is inserted whenever smokeping is started up.
+
+You can write
+
+ ==S,>20%,>20%
+
+to detect lines that have been losing more than 20% of the packets for two
+periods after startup.
+
+Sometimes it may be that conditions occur at irregular intervals. But still
+you only want to throw an alert if they occur several times within a certain
+amount of times. The operator B<*X*> will ignore up to I<X> values and still
+let the pattern match:
+
+ >10%,*10*,>10%
+
+will fire if more than 10% of the packets have been losst twice over the
+last 10 samples.
+
+A complete example
+
+ *** Alerts ***
+ to = admin\@company.xy,peter\@home.xy
+ from = smokealert\@company.xy
+
+ +lossdetect
+ type = loss
+ # in percent
+ pattern = ==0%,==0%,==0%,==0%,>20%,>20%,>20%
+ comment = suddenly there is packet loss
+
+ +miniloss
+ type = loss
+ # in percent
+ pattern = >0%,*12*,>0%,*12*,>0%
+ comment = detected loss 3 times over the last two hours
+
+ +rttdetect
+ type = rtt
+ # in milliseconds
+ pattern = <10,<10,<10,<10,<10,<100,>100,>100,>100
+ comment = routing messed up again ?
+
+ +rttbadstart
+ type = rtt
+ # in milliseconds
+ pattern = ==S,==U
+ comment = offline at startup
+
+DOC
+
+ _sections => [ '/[^\s,]+/' ],
+ _vars => [ qw(to from) ],
+ _mandatory => [ qw(to from)],
+ to => { doc => <<DOC,
+Either an email address to send alerts to, or the name of a program to
+execute when an alert matches. To call a program, the first character of the
+B<to> value must be a pipe symbol "|". The program will the be called
+whenever an alert matches, using the following 5 arguments:
+B<name-of-alert>, B<target>, B<loss-pattern>, B<rtt-pattern>, B<hostname>.
+You can also provide a comma separated list of addresses and programs.
+DOC
+ _re => '(\|.+|.+@\S+|snpp:)',
+ _re_error => 'put an email address or the name of a program here',
+ },
+ from => { doc => 'who should alerts appear to be coming from ?',
+ _re => '.+@\S+',
+ _re_error => 'put an email address here',
+ },
+ '/[^\s,]+/' => {
+ _vars => [ qw(type pattern comment to) ],
+ _mandatory => [ qw(type pattern comment) ],
+ to => { doc => 'Similar to the "to" parameter on the top-level except that it will only be used IN ADDITION to the value of the toplevel parameter. Same rules apply.',
+ _re => '(\|.+|.+@\S+|snpp:)',
+ _re_error => 'put an email address or the name of a program here',
+ },
+
+ type => {
+ _doc => 'Currently the pattern types B<rtt> and B<loss> and B<matcher> are known',
+ _re => '(rtt|loss|matcher)',
+ _re_error => 'Use loss or rtt'
+ },
+ pattern => {
+ _doc => "a comma separated list of comparison operators and numbers. rtt patterns are in milliseconds, loss patterns are in percents",
+ _re => '(?:([^,]+)(,[^,]+)*|\S+\(.+\s)',
+ _re_error => 'Could not parse pattern or matcher',
+ },
+ },
+ },
+ Targets => {_doc => <<DOC,
+The Target Section defines the actual work of SmokePing. It contains a hierarchical list
+of hosts which mark the endpoints of the network connections the system should monitor.
+Each section can contain one host as well as other sections.
+DOC
+ _vars => [ qw(probe menu title remark alerts) ],
+ _mandatory => [ qw(probe menu title) ],
+ _order => 1,
+ _sections => [ ( "PROBE_CONF", "/$KEY_RE/" ) ],
+ probe => { _doc => <<DOC },
+The name of the probe module to be used for this host. The value of
+this variable gets propagated
+DOC
+ PROBE_CONF => $PROBEVARS,
+ menu => { _doc => <<DOC },
+Menu entry for this section. If not set this will be set to the hostname.
+DOC
+ alerts => { _doc => <<DOC },
+A comma separated list of alerts to check for this target. The alerts have
+to be setup in the Alerts section. Alerts are inherited by child nodes. Use
+an empty alerts definition to remove inherited alerts from the current target
+and its children.
+
+DOC
+ title => { _doc => <<DOC },
+Title of the page when it is displayed. This will be set to the hostname if
+left empty.
+DOC
+
+ remark => { _doc => <<DOC },
+An optional remark on the current section. It gets displayed on the webpage.
+DOC
+
+ "/$KEY_RE/" => $TARGET
+ }
+
+ }
+ );
+ return $parser;
+}
+
+sub get_config ($$){
+ my $parser = shift;
+ my $cfgfile = shift;
+
+ return $parser->parse( $cfgfile ) || die "ERROR: $parser->{err}\n";
+}
+
+sub kill_smoke ($) {
+ my $pidfile = shift;
+ if (defined $pidfile){
+ if ( -f $pidfile && open PIDFILE, "<$pidfile" ) {
+ <PIDFILE> =~ /(\d+)/;
+ my $pid = $1;
+ kill 2, $pid if kill 0, $pid;
+ sleep 3; # let it die
+ die "ERROR: Can not stop running instance of SmokePing ($pid)\n"
+ if kill 0, $pid;
+ close PIDFILE;
+ } else {
+ die "ERROR: Can not read pid from $pidfile: $!\n";
+ };
+ }
+}
+
+sub daemonize_me ($) {
+ my $pidfile = shift;
+ if (defined $pidfile){
+ if (-f $pidfile ) {
+ open PIDFILE, "<$pidfile";
+ <PIDFILE> =~ /(\d+)/;
+ close PIDFILE;
+ my $pid = $1;
+ die "ERROR: I Quit! Another copy of $0 ($pid) seems to be running.\n".
+ " Check $pidfile\n"
+ if kill 0, $pid;
+ }
+ }
+ print "Warning: no logging method specified. Messages will be lost.\n"
+ unless $logging;
+ print "Daemonizing $0 ...\n";
+ defined (my $pid = fork) or die "Can't fork: $!";
+ if ($pid) {
+ exit;
+ } else {
+ if(open(PIDFILE,">$pidfile")){
+ print PIDFILE "$$\n";
+ close PIDFILE;
+ } else {
+ warn "creating $pidfile: $!\n";
+ };
+ require 'POSIX.pm';
+ &POSIX::setsid or die "Can't start a new session: $!";
+ open STDOUT,'>/dev/null' or die "ERROR: Redirecting STDOUT to /dev/null: $!";
+ open STDIN, '</dev/null' or die "ERROR: Redirecting STDIN from /dev/null: $!";
+ open STDERR, '>/dev/null' or die "ERROR: Redirecting STDERR to /dev/null: $!";
+ # send warnings and die messages to log
+ $SIG{__WARN__} = sub { do_log ((shift)."\n") };
+ $SIG{__DIE__} = sub { do_log ((shift)."\n"); exit 1 };
+ }
+}
+
+# pseudo log system object
+{
+ my $use_syslog;
+ my $use_cgilog;
+ my $use_debuglog;
+ my $use_filelog;
+
+ my $syslog_facility;
+ my $syslog_priority = $DEFAULTPRIORITY;
+
+ sub initialize_debuglog (){
+ $use_debuglog = 1;
+ }
+
+ sub initialize_cgilog (){
+ $use_cgilog = 1;
+ $logging=1;
+ }
+
+ sub initialize_filelog ($){
+ $use_filelog = shift;
+ $logging=1;
+ }
+
+ sub initialize_syslog ($$) {
+ my $fac = shift;
+ my $pri = shift;
+ $use_syslog = 1;
+ $logging=1;
+ die "missing facility?" unless defined $fac;
+ $syslog_facility = $fac if defined $fac;
+ $syslog_priority = $pri if defined $pri;
+ print "Note: logging to syslog as $syslog_facility/$syslog_priority.\n";
+ openlog(basename($0), 'pid', $syslog_facility);
+ }
+
+ sub do_syslog ($){
+ syslog("$syslog_facility|$syslog_priority", shift);
+ }
+
+ sub do_cgilog ($){
+ my $str = shift;
+ print "<p>" , $str, "</p>\n";
+ print STDERR $str,"\n"; # for the webserver log
+ }
+
+ sub do_debuglog ($){
+ do_log(shift) if $use_debuglog;
+ }
+
+ sub do_filelog ($){
+ open X,">>$use_filelog" or return;
+ print X scalar localtime(time)," - ",shift,"\n";
+ close X;
+ }
+
+ sub do_log (@){
+ my $string = join(" ", @_);
+ chomp $string;
+ do_syslog($string) if $use_syslog;
+ do_cgilog($string) if $use_cgilog;
+ do_filelog($string) if $use_filelog;
+ print STDERR $string,"\n" unless $logging;
+ }
+
+}
+
+###########################################################################
+# The Main Program
+###########################################################################
+
+my $RCS_VERSION = '$Id: Smokeping.pm,v 1.5 2004/10/21 21:10:51 oetiker Exp $';
+
+sub load_cfg ($) {
+ my $cfgfile = shift;
+ my $cfmod = (stat $cfgfile)[9] || die "ERROR: calling stat on $cfgfile: $!\n";
+ # when running under speedy this will prevent reloading on every run
+ # if cfgfile has been modified we will still run.
+ if (not defined $cfg or $cfg->{__last} < $cfmod ){
+ $cfg = undef;
+ my $parser = get_parser;
+ $cfg = get_config $parser, $cfgfile;
+ $cfg->{__parser} = $parser;
+ $cfg->{__last} = $cfmod;
+ $cfg->{__cfgfile} = $cfgfile;
+ $probes = undef;
+ $probes = load_probes $cfg;
+ $cfg->{__probes} = $probes;
+ init_alerts $cfg if $cfg->{Alerts};
+ init_target_tree $cfg, $probes, $cfg->{Targets}{probe}, $cfg->{Targets}, $cfg->{General}{datadir}, $cfg->{Targets}{PROBE_CONF},$cfg->{Targets}{alerts},undef;
+ }
+}
+
+
+sub makepod ($){
+ my $parser = shift;
+ my $e='=';
+ print <<POD;
+
+${e}head1 NAME
+
+smokeping_config - Reference for the SmokePing Config File
+
+${e}head1 OVERVIEW
+
+SmokePing takes its configuration from a single central configuration file.
+Its location must be hardcoded in the smokeping script and smokeping.cgi.
+
+The contents of this manual is generated directly from the configuration
+file parser.
+
+The Parser for the Configuration file is written using David Schweikers
+ParseConfig module. Read all about it in L<ISG::ParseConfig>.
+
+The Configuration file has a tree-like structure with section headings at
+various levels. It also contains variable assignments and tables.
+
+${e}head1 REFERENCE
+
+The text below describes the syntax of the SmokePing configuration file.
+
+POD
+
+ print $parser->makepod;
+ print <<POD;
+
+${e}head1 COPYRIGHT
+
+Copyright (c) 2001-2003 by Tobias Oetiker. All right reserved.
+
+${e}head1 LICENSE
+
+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., 675 Mass Ave, Cambridge, MA
+02139, USA.
+
+${e}head1 AUTHOR
+
+Tobias Oetiker E<lt>tobi\@oetiker.chE<gt>
+
+${e}cut
+POD
+ exit 0;
+
+
+}
+sub cgi ($) {
+ $cgimode = 'yes';
+ # make sure error are shown in appropriate manner even when running from speedy
+ # and thus not getting BEGIN re-executed.
+ if ($ENV{SERVER_SOFTWARE}) {
+ $SIG{__WARN__} = sub { print "Content-Type: text/plain\n\n".(shift)."\n"; };
+ $SIG{__DIE__} = sub { print "Content-Type: text/plain\n\n".(shift)."\n"; exit 1 }
+ };
+ umask 022;
+ load_cfg shift;
+ my $q=new CGI;
+ print $q->header(-type=>'text/html',
+ -expires=>'+'.($cfg->{Database}{step}).'s',
+ -charset=> ( $cfg->{Presentation}{charset} || 'iso-8859-15')
+ );
+ if ($ENV{SERVER_SOFTWARE}) {
+ $SIG{__WARN__} = sub { print "<pre>".(shift)."</pre>"; };
+ $SIG{__DIE__} = sub { print "<pre>".(shift)."</pre>"; exit 1 }
+ };
+ initialize_cgilog();
+ if ($q->param(-name=>'secret') && $q->param(-name=>'target') ) {
+ update_dynaddr $cfg,$q;
+ } else {
+ display_webpage $cfg,$q;
+ }
+}
+
+
+sub gen_page ($$$);
+sub gen_page ($$$) {
+ my ($cfg, $tree, $open) = @_;
+ my ($q, $name, $page);
+
+ $q = bless \$q, 'dummyCGI';
+
+ $name = @$open ? join('.', @$open) . ".html" : "index.html";
+
+ die "Can not open $cfg-{General}{pagedir}/$name for writing: $!" unless
+ open PAGEFILE, ">$cfg->{General}{pagedir}/$name";
+
+ my $step = $probes->{$tree->{probe}}->step();
+
+ $page = fill_template
+ ($cfg->{Presentation}{template},
+ {
+ menu => target_menu($cfg->{Targets},
+ [@$open], #copy this because it gets changed
+ "", ".html"),
+ title => $tree->{title},
+ remark => ($tree->{remark} || ''),
+ overview => get_overview( $cfg,$q,$tree,$open ),
+ body => get_detail( $cfg,$q,$tree,$open ),
+ target_ip => ($tree->{host} || ''),
+ owner => $cfg->{General}{owner},
+ contact => $cfg->{General}{contact},
+ author => '<A HREF="http://tobi.oetiker.ch/">Tobi&nbsp;Oetiker</A>',
+ smokeping => '<A HREF="http://people.ee.ethz.ch/~oetiker/webtools/smokeping/counter.cgi/'.$VERSION.'">SmokePing-'.$VERSION.'</A>',
+ step => $step,
+ rrdlogo => '<A HREF="http://people.ee.ethz.ch/~oetiker/webtools/rrdtool/"><img border="0" src="'.$cfg->{General}{imgurl}.'/rrdtool.png"></a>',
+ smokelogo => '<A HREF="http://people.ee.ethz.ch/~oetiker/webtools/smokeping/counter.cgi/'.$VERSION.'"><img border="0" src="'.$cfg->{General}{imgurl}.'/smokeping.png"></a>',
+ });
+
+ print PAGEFILE $page;
+ close PAGEFILE;
+
+ foreach my $key (keys %$tree) {
+ my $value = $tree->{$key};
+ next unless ref($value) eq 'HASH';
+ gen_page($cfg, $value, [ @$open, $key ]);
+ }
+}
+
+sub makestaticpages ($$) {
+ my $cfg = shift;
+ my $dir = shift;
+
+ # If directory is given, override current values (pagedir and and
+ # imgurl) so that all generated data is in $dir. If $dir is undef,
+ # use values from config file.
+ if ($dir) {
+ mkdir $dir, 0755 unless -d $dir;
+ $cfg->{General}{pagedir} = $dir;
+ $cfg->{General}{imgurl} = '.';
+ }
+
+ die "ERROR: No pagedir defined for static pages\n"
+ unless $cfg->{General}{pagedir};
+ # Logos.
+ gen_imgs($cfg);
+
+ # Iterate over all targets.
+ my $tree = $cfg->{Targets};
+ gen_page($cfg, $tree, []);
+}
+
+sub pages ($) {
+ my ($config) = @_;
+ umask 022;
+ load_cfg($config);
+ makestaticpages($cfg, undef);
+}
+
+sub main ($) {
+ $cgimode = 0;
+ umask 022;
+ my $cfgfile = shift;
+ $opt{filter}=[];
+ GetOptions(\%opt, 'version', 'email', ,'man','help','logfile=s','static-pages:s', 'debug-daemon',
+ 'nosleep', 'makepod','debug','restart', 'filter=s', 'nodaemon|nodemon') or pod2usage(2);
+ if($opt{version}) { print "$RCS_VERSION\n"; exit(0) };
+ if($opt{man}) { pod2usage(-verbose => 2); exit 0 };
+ if($opt{help}) { pod2usage(-verbose => 1); exit 0 };
+ if($opt{makepod}) { makepod(get_parser) ; exit 0};
+ initialize_debuglog if $opt{debug} or $opt{'debug-daemon'};
+ load_cfg $cfgfile;
+ if(defined $opt{'static-pages'}) { makestaticpages $cfg, $opt{'static-pages'}; exit 0 };
+ if($opt{email}) { enable_dynamic $cfg, $cfg->{Targets},"",""; exit 0 };
+ if($opt{restart}) { kill_smoke $cfg->{General}{piddir}."/smokeping.pid";};
+ if($opt{logfile}) { initialize_filelog($opt{logfile}) };
+ if (not keys %$probes) {
+ do_log("No probes defined, exiting.");
+ exit 1;
+ }
+ unless ($opt{debug} or $opt{nodaemon}) {
+ if (defined $cfg->{General}{syslogfacility}) {
+ initialize_syslog($cfg->{General}{syslogfacility},
+ $cfg->{General}{syslogpriority});
+ }
+ daemonize_me $cfg->{General}{piddir}."/smokeping.pid";
+ }
+ do_log "Launched successfully";
+
+ my $myprobe;
+ my $forkprobes = $cfg->{General}{concurrentprobes} || 'yes';
+ if ($forkprobes eq "yes" and keys %$probes > 1 and not $opt{debug}) {
+ my %probepids;
+ my $pid;
+ do_log("Entering multiprocess mode.");
+ for my $p (keys %$probes) {
+ if ($probes->{$p}->target_count == 0) {
+ do_log("No targets defined for probe $p, skipping.");
+ next;
+ }
+ my $sleep_count = 0;
+ do {
+ $pid = fork;
+ unless (defined $pid) {
+ do_log("Fatal: cannot fork: $!");
+ die "bailing out"
+ if $sleep_count++ > 6;
+ sleep 10;
+ }
+ } until defined $pid;
+ $myprobe = $p;
+ goto KID unless $pid; # child skips rest of loop
+ do_log("Child process $pid started for probe $myprobe.");
+ $probepids{$pid} = $myprobe;
+ }
+ # parent
+ do_log("All probe processes started succesfully.");
+ my $exiting = 0;
+ for my $sig (qw(INT TERM)) {
+ $SIG{$sig} = sub {
+ do_log("Got $sig signal, terminating child processes.");
+ $exiting = 1;
+ kill $sig, $_ for keys %probepids;
+ my $now = time;
+ while(keys %probepids) { # SIGCHLD handler below removes the keys
+ if (time - $now > 2) {
+ do_log("Can't terminate all child processes, giving up.");
+ exit 1;
+ }
+ sleep 1;
+ }
+ do_log("All child processes succesfully terminated, exiting.");
+ exit 0;
+ }
+ };
+ $SIG{CHLD} = sub {
+ while ((my $dead = waitpid(-1, WNOHANG)) > 0) {
+ my $p = $probepids{$dead};
+ $p = 'unknown' unless defined $p;
+ do_log("Child process $dead (probe $p) exited unexpectedly with status $?.")
+ unless $exiting;
+ delete $probepids{$dead};
+ }
+ };
+ sleep while 1; # just wait for the signals
+ do_log("Exiting abnormally - this should not happen.");
+ exit 1; # not reached
+ } else {
+ if ($forkprobes ne "yes") {
+ do_log("Not entering multiprocess mode because the 'concurrentprobes' variable is not set.");
+ for my $p (keys %$probes) {
+ for my $what (qw(offset step)) {
+ do_log("Warning: probe-specific parameter '$what' ignored for probe $p in single-process mode." )
+ if defined $cfg->{Probes}{$p}{$what};
+ }
+ }
+ } elsif ($opt{debug}) {
+ do_debuglog("Not entering multiprocess mode with '--debug'. Use '--debug-daemon' for that.")
+ } elsif (keys %$probes == 1) {
+ do_log("Not entering multiprocess mode for just a single probe.");
+ $myprobe = (keys %$probes)[0]; # this way we won't ignore a probe-specific step parameter
+ }
+ for my $sig (qw(INT TERM)) {
+ $SIG{$sig} = sub {
+ do_log("Got $sig signal, terminating.");
+ exit 1;
+ }
+ }
+ }
+KID:
+ my $offset;
+ my $step;
+ if (defined $myprobe) {
+ $offset = $probes->{$myprobe}->offset || 'random';
+ $step = $probes->{$myprobe}->step;
+ $0 .= " [$myprobe]" unless defined $cfg->{General}{changeprocessnames}
+ and $cfg->{General}{changeprocessnames} eq "no";
+ } else {
+ $offset = $cfg->{General}{offset} || 'random';
+ $step = $cfg->{Database}{step};
+ }
+ if ($offset eq 'random'){
+ $offset = int(rand($step));
+ } else {
+ $offset =~ s/%$//;
+ $offset = $offset / 100 * $step;
+ }
+ for (keys %$probes) {
+ next if defined $myprobe and $_ ne $myprobe;
+ # fill this in for report_probes() below
+ $probes->{$_}->offset_in_seconds($offset); # this is just for humans
+ if ($opt{debug} or $opt{'debug-daemon'}) {
+ $probes->{$_}->debug(1) if $probes->{$_}->can('debug');
+ }
+ }
+
+ report_probes($probes, $myprobe);
+
+ while (1) {
+ unless ($opt{nosleep} or $opt{debug}) {
+ my $sleeptime = $step - (time-$offset) % $step;
+ if (defined $myprobe) {
+ $probes->{$myprobe}->do_debug("Sleeping $sleeptime seconds.");
+ } else {
+ do_debuglog("Sleeping $sleeptime seconds.");
+ }
+ sleep $sleeptime;
+ }
+ my $now = time;
+ run_probes $probes, $myprobe; # $myprobe is undef if running without 'concurrentprobes'
+ update_rrds $cfg, $probes, $cfg->{Targets}{probe}, $cfg->{Targets}, $cfg->{General}{datadir}, $myprobe;
+ exit 0 if $opt{debug};
+ my $runtime = time - $now;
+ if ($runtime > $step) {
+ my $warn = "WARNING: smokeping took $runtime seconds to complete 1 round of polling. ".
+ "It should complete polling in $step seconds. ".
+ "You may have unresponsive devices in your setup.\n";
+ if (defined $myprobe) {
+ $probes->{$myprobe}->do_log($warn);
+ } else {
+ do_log($warn);
+ }
+ }
+ }
+}
+
+sub gen_imgs ($){
+
+ my $cfg = shift;
+ if (not -r $cfg->{General}{imgcache}."/rrdtool.png"){
+open W, ">".$cfg->{General}{imgcache}."/rrdtool.png"
+ or do { warn "WARNING: creating $cfg->{General}{imgcache}/rrdtool.png: $!\n"; return 0 };
+print W unpack ('u', <<'UUENC');
+MB5!.1PT*&@H -24A$4@ '@ B! , !F7P!P +5!,5$44&5T0
+M.(L@2)8N6* X9JQ)=+)QC[Q7AL"9J;63KL^SR=[]^\S____K^?S6XN6'_*P9
+M &D$E$051XVIV5_T\;YQW'37Z(M"Z+>!XW3B&VQ3U'$ 6&N#L;D9JAF+MK
+M:(FB4M\%.C,4DX8HX"%-PK+29M$@0:,CL@H$4F<TBE&:U!VM-&,(433$D>#%
+M["Y:VB3J-AOA X97_Z&?1XS;=I^V _[G.\>VWI>S_MY/L_[^9QIYW_%\FZS
+M3>_MG>5M>&YO+L_]81/:OZ5G3&O!YB!$(-B<"U7M46595A1%EJ5<B! .0>!Y
+M@65YGO#DGX%1E6D]^-]!!Z/#T"L04',APX@>59(E17(ZG;(31A.$HZ:70;7Y
+M/]#F8" 0""J2),,T)%55)$41!5GQP%R D"2GX'#\"[X_1Z&/SSX+Y-!=V<'[
+M]Z=!,2ZK7IC_X)P(3U$0I[ULCA-<K"#0:6<_O@;$!X'X^3BTZC%54=5 T'OV
+M0X]3CDE2@U-JF&Z;$GBALI?O\YIY ;,\^2%" +\,QD^LWPZ^_"80OSAW)JB$
+MOFBXF 7-<L73EG7,;@A_=\@N#S]].-3GG:M9;32O"3OO5]:U6'9X4 [<VU3N
+M*<^Z>IY^<#>@MMW=K)OY+>3'I<HS7:=GNB8&':*WN'ZVO'_6WE\2\Q[J\W9,
+MEV>)JV47OG'K7L]T8T_\G7BSVBBY3EQV09Y<2MUJS>JWCIBW6.PJKM\("5WV
+M/B[F#:$-/G0(D_*CYB* GP;B&PJ%6]8"S5ZGJZT?5.5RM>YRS8M8_51CL=AH
+MYF8V=S;LO=R4:Q/-[6P<POR!^X0JQ^7X*Y'";T_)JDMRU8$RP)Z&U<+56'&L
+MD17+?<53(83LO4S,]<;*1EX>*.?O[P5E]=[FK=^%[C;VS!V;O:R>F)FKO.Q2
+M9,FE.&8'Q=G!2UY6J)X=['4-3)7.-L6\*.X:^'VY&9=W_:K*M-8<OB8WW!EM
+M4-JD[JN*I_M2]8<-X*$:C]C2[QCX-5LC"%S;.DM"[<Q 4U\IKC8/^$KS2&D6
+M%YG6Z+:"E7M4<!*XCX8#;L$IB.#HW$?@B,!BL#5#&)9A$$($YR,",!AXU[U
+MRY*ZBXO4PD").3O1,\&RA%X, T>"P"B8W9-3IGW!NR+8SP&BX#]6$"D&O5E"
+M6%X ,4+/$Q5%A&IC& 3@=\Z';T3''/*Q.^'KT;%ZASAZKB-\E@!;.M9!HYUE
+M#GZ]Y*-ZZ,A6%&%LJ3[9A FL^;L3=_3LYWK"(2]L):_KC_GZQ 6C4W\.=)F^
+M/:$OZ1E2>&&Q1A_"#.K<.NU?0HRU1>]E"%]D>O%N^/B(,)[N<3S\2".UZ8S[
+M^=4DMNG# ENP:(E6W*S(,'_=QR *V5>>H]?U#+)U'/<1NN;5M\*=(])/4JON
+MQ8D%\4WC ??J3)0M,1:)8(\4C)V,6)[80[4(V5(^YL8=#>5O+R';N=:CD+8<
+M[!^1?IR:<#_H7G"4 )R]EC0SQK*9KR26L=8(LU)Q8&>/VVYH2/OIGXN9SU/N
+M?257?(3DX*OZB&A-G79K6YI08FA<]G@2<]LIJ%?DC2$]@KAH1<*>+C 2EDPJ
+M,OG,GQZR'M:;"(:$?5LV,CDBUCYF.>UGFE"HO\_=KDR: 895\9:;_IL$/2U;
+MS#=9C&1A^_BG^:;:](K-G6K*;=7JD? OA\GDL,!ISS74NHZY[-A?,+.=&B)5
+MG.6+3R*XZ(\CBU=TV5C>_Y%U5$]4I#7;NQ6GP"=4^:O*L:\39H%+>K1O#,1R
+MKX:S%/9!2BS7W\X0I*TMFKF]QK+]?/>GQ4QM>M[VF;4IE[ 7;XY.MNMI LH_
+MU\93O83)'DE2^!3A&,N$/X.+'IY+%$0+C$>OG?5'.H=;TT]^A+<X\%F1Z4G9
+ML#[D3T581IO4K.D%S&0KDJC(2#'L'F*YJ4<(^L2:+(@6IA\>6$E%KD?\*9_-
+MHS=AY*;PZ/B0S5ADN?F+\P>-QYBY?3*!D?$G\#"QW&J-$*;"LHS0:[!5#SKG
+M$1I/<39Y'*:M%9DR9>&WA@X;*4P6VS6Z5)1M_Q)9C =P[,C!+ZM7&*;@>RM"
+MUM0OD/_2 D)&$NWKL#8A6P:4[><N##%;Z0S6PAID8P&MCR:1'>8%RC^X[<\0
+MDC\)?2>3>YB]6A+MU3/86GKA/;05 =CZF?Z"J]4?631] >W7]:KOKSPZHG\'
+MPIBQ9_5YS# 6_U+W8Q_&Z*2>W4H@LJ]37]9UJGR8,[N)F7?B]Q!/$%0-MW"F
+MICTO]R:$UP,'#3+_)EJ%Z)>2K_KS,#&_3DL$!VOFZ7FGM2$7M%+ 8<?DWS])
+GK@CL_D-K0>[]"@'[?,KT_\<_ *X%"4UQ:&PM $E%3D2N0F""
+UUENC
+close W;
+}
+
+ if (not -r $cfg->{General}{imgcache}."/smokeping.png"){
+open W, ">".$cfg->{General}{imgcache}."/smokeping.png"
+ or do { warn "WARNING: creating $cfg->{General}{imgcache}/smokeping.png: $!\n"; return 0};
+print W unpack ('u', <<'UUENC');
+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
+*``!)14Y$KD)@@@``
+UUENC
+close W;
+}
+}
+
+
+=head1 NAME
+
+Smokeping.pm - SmokePing Perl Module
+
+=head1 OVERVIEW
+
+Almost all SmokePing functionality sits in this Module.
+The programs B<smokeping> and B<smokeping.cgi> are merely
+figure heads allowing to hardcode some pathnames.
+
+If you feel like documenting what is happening within this library you are
+most welcome todo so.
+
+=head1 COPYRIGHT
+
+Copyright (c) 2001 by Tobias Oetiker. All right reserved.
+
+=head1 LICENSE
+
+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., 675 Mass Ave, Cambridge, MA
+02139, USA.
+
+=head1 AUTHOR
+
+Tobias Oetiker E<lt>tobi\@oetiker.chE<gt>
+
+=cut
+
+# Emacs Configuration
+#
+# Local Variables:
+# mode: cperl
+# eval: (cperl-set-style "PerlStyle")
+# mode: flyspell
+# mode: flyspell-prog
+# End:
+#
+# vi: sw=4