This commit is contained in:
root
2025-11-07 11:31:06 +00:00
commit 2859f93882
407 changed files with 99769 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,152 @@
# -*- perl -*-
package Smokeping::Colorspace;
=head1 NAME
Smokeping::Colorspace - Simple Colorspace Conversion methods
=head1 OVERVIEW
This module provides simple colorspace conversion methods, primarily allowing
conversion from RGB (red, green, blue) to and from HSL (hue, saturation, luminosity).
=head1 COPYRIGHT
Copyright 2006 by Grahame Bowland.
=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
Grahame Bowland <grahame.bowland@uwa.edu.au>
=cut
sub web_to_rgb {
my $web = shift;
$web =~ s/^#//;
my @rgb = (hex(substr($web, 0, 2)) / 255,
hex(substr($web, 2, 2)) / 255,
hex(substr($web, 4, 2)) / 255) ;
return @rgb;
}
sub rgb_to_web {
my @rgb = @_;
return sprintf("#%.2x%.2x%.2x", 255 * $rgb[0], 255 * $rgb[1], 255 * $rgb[2]);
}
sub min_max_indexes {
my $idx = 0;
my ($min_idx, $min, $max_idx, $max);
my @l = @_;
foreach my $i (@l) {
if (not defined($min) or ($i < $min)) {
$min = $i;
$min_idx = $idx;
}
if (not defined($max) or ($i > $max)) {
$max = $i;
$max_idx = $idx;
}
$idx++;
}
return ($min_idx, $min, $max_idx, $max);
}
# source for conversion algorithm is:
# http://www.easyrgb.com/math.php?MATH=M18#text18
sub rgb_to_hsl {
my @rgb = @_;
my ($h, $l, $s);
my ($min_idx, $min, $max_idx, $max) = min_max_indexes(@rgb);
my $delta_max = $max - $min;
$l = ($max + $min) / 2;
if ($delta_max == 0) {
my $h = 0;
my $s = 0;
} else {
if ($l < 0.5) {
$s = $delta_max / ($max + $min);
} else {
$s = $delta_max / (2 - $max - $min);
}
my $delta_r = ((($max - $rgb[0]) / 6) + ($max / 2)) / $delta_max;
my $delta_g = ((($max - $rgb[1]) / 6) + ($max / 2)) / $delta_max;
my $delta_b = ((($max - $rgb[2]) / 6) + ($max / 2)) / $delta_max;
if ($max_idx == 0) {
$h = $delta_b - $delta_g;
} elsif ($max_idx == 1) {
$h = (1/3) + $delta_r - $delta_b;
} else {
$h = (2/3) + $delta_g - $delta_r;
}
if ($h < 0) {
$h += 1;
} elsif ($h > 1) {
$h -= 1;
}
}
return ($h, $s, $l);
}
sub hue_to_rgb {
my ($v1, $v2, $vh) = @_;
if ($vh < 0) {
$vh += 1;
} elsif ($vh > 1) {
$vh -= 1;
}
if ($vh * 6 < 1) {
return $v1 + ($v2 - $v1) * 6 * $vh;
} elsif ($vh * 2 < 1) {
return $v2;
} elsif ($vh * 3 < 2) {
return $v1 + ($v2 - $v1) * ((2/3) - $vh) * 6;
} else {
return $v1;
}
}
sub hsl_to_rgb {
my ($h, $s, $l) = @_;
my ($r, $g, $b);
if ($s == 0) {
$r = $g = $b = $l;
} else {
my $ls;
if ($l < 0.5) {
$ls = $l * (1 + $s);
} else {
$ls = ($l + $s) - ($s * $l);
}
$l = 2 * $l - $ls;
$r = hue_to_rgb($l, $ls, $h + 1/3);
$g = hue_to_rgb($l, $ls, $h);
$b = hue_to_rgb($l, $ls, $h - (1/3));
}
return ($r, $g, $b);
}
1;

View File

@@ -0,0 +1,15 @@
# provide backward compatibility for Config::Grammar
package Smokeping::Config;
BEGIN {
require Config::Grammar;
if($Config::Grammar::VERSION ge '1.10') {
require Config::Grammar::Dynamic;
@ISA = qw(Config::Grammar::Dynamic);
}
else {
@ISA = qw(Config::Grammar);
}
}
1;

View File

@@ -0,0 +1,678 @@
# -*- perl -*-
package Smokeping::Examples;
use strict;
use Smokeping;
=head1 NAME
Smokeping::Examples - A module for generating the smokeping_examples document
=head1 OVERVIEW
This module generates L<smokeping_examples> and the example
configuration files distributed with Smokeping. It is supposed to be
invoked from the smokeping distribution top directory, as it will need
the C<etc/config.dist> template configuration file and will create files
in the directories C<doc> and C<doc/examples>.
=head1 DESCRIPTION
The entry point to the module is the C<make> subroutine. It takes one optional
parameter, C<check>, that makes the module run a syntax check for all the
created example configuration files.
=head1 BUGS
This module uses more or less internal functions from L<Smokeping.pm|Smokeping>. It's a
separate module only because the latter is much too big already.
It should be possible to include POD markup in the configuration explanations
and have this module filter them away for the config files.
It might be nice for the probe module authors to be able to provide an
example configuration as part of the probe module instead of having to
modify Smokeping::Examples too.
=head1 COPYRIGHT
Copyright 2005 by Niko Tyni.
=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
Niko Tyni <ntyni@iki.fi>
=cut
use strict;
sub read_config_template {
my $file = "../etc/config.dist";
my $h = {
common => "", # everything up to the Probes section
probes => "", # the Probes section, without the *** Probes *** line
targets => "", # the Targets section, without the *** Targets *** line
};
open(F, "<$file") or die("open template configuration file $file for reading: $!");
my %found;
while (<F>) {
/\*\*\*\s*(Probes|Targets)\s*\*\*\*/ and $found{$1} = 1, next;
$h->{common} .= $_ and next unless $found{Probes};
$h->{probes} .= $_ and next unless $found{Targets};
$h->{targets} .= $_;
}
close F;
return $h;
}
sub prologue {
my $e = "=";
return <<DOC;
${e}head1 NAME
smokeping_examples - Examples of Smokeping configuration
${e}head1 OVERVIEW
This document provides some examples of Smokeping configuration files.
All the examples can be found in the C<examples> directory in the
Smokeping documentation. Note that the DNS names in the examples are
non-functional.
Details of the syntax and all the variables are found in
L<smokeping_config> and in the documentation of the
corresponding probe, if applicable.
This manual is automatically generated from the Smokeping source code,
specifically the L<Smokeping::Examples|Smokeping::Examples> module.
${e}head1 DESCRIPTION
Currently the examples differ only in the C<Probes> and C<Targets>
sections. The other sections are taken from the C<etc/config.dist>
configuration template in the Smokeping distribution so that the example
files are complete.
If you would like to provide more examples, document the other sections
or enhance the existing examples, please do so, preferably by sending
the proposed changes to the smokeping-users mailing list.
DOC
}
sub epilogue {
my $e = "=";
return <<DOC;
${e}head1 COPYRIGHT
Copyright 2005 by Niko Tyni.
${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
Niko Tyni <ntyni\@iki.fi>
${e}head1 SEE ALSO
The other Smokeping documents, especially L<smokeping_config>.
DOC
}
sub make {
print "Generating example files...\n";
my $check = shift; # check the syntax of the generated config files
my $template = read_config_template();
my $examples = examples($template);
my $manual = prologue();
for my $ex (sort { $examples->{$a}{order} <=> $examples->{$b}{order} } keys %$examples) {
my $h = $examples->{$ex};
$manual .= "\n=head2 Example $h->{order}: config.$ex\n\n"
. genpod($h);
my $cfgfile = "examples/config.$ex";
print "\t$cfgfile ...\n";
writecfg($cfgfile, $template, $h);
if ($check) {
local $Smokeping::cfg = undef;
eval {
Smokeping::verify_cfg($cfgfile);
};
die("Syntax check for $cfgfile failed: $@") if $@;
}
}
$manual .= epilogue();
writemanual($manual);
print "done.\n";
}
sub writemanual {
my $text = shift;
my $filename = "smokeping_examples.pod";
print "\t$filename ...\n";
open(F, ">$filename") or die("open $filename for writing: $!");
print F $text;
close F;
}
sub genpod {
my $h = shift;
my $text = "";
$text .= "=over\n\n";
$text .= "=item Probe configuration\n\n";
$text .= " *** Probes ***\n";
$text .= join("\n", map { " $_" } split(/\n/, $h->{probes}));
$text .= "\n\n=item Probe explanation\n\n";
$text .= $h->{probedoc} || "No probedoc found !";
$text .= "\n\n=item Target configuration\n\n";
$text .= " *** Targets ***\n";
$text .= join("\n", map { " $_" } split(/\n/, $h->{targets}));
$text .= "\n\n=item Target explanation\n\n";
$text .= $h->{targetdoc} || "No targetdoc found !";
$text .= "\n\n=back\n\n";
return $text;
}
sub writecfg {
my $file = shift;
my $template = shift;
my $h = shift;
open(F, ">$file") or die("open $file for writing: $!");
print F <<DOC;
# This Smokeping example configuration file was automatically generated.
#
# Everything up to the Probes section is derived from a common template file.
# See the Probes and Targets sections for the actual example.
#
# This example is included in the smokeping_examples document.
DOC
print F $template->{common};
print F "# (The actual example starts here.)\n";
print F "\n*** Probes ***\n\n";
print F join("\n", map { "# $_" } split(/\n/, $h->{probedoc} || 'No probedoc found!'));
print F "\n\n";
print F $h->{probes};
print F "\n*** Targets ***\n\n";
print F join("\n", map { "# $_" } split(/\n/, $h->{targetdoc} || 'No targetdoc found'));
print F "\n\n";
print F $h->{targets};
close F;
}
sub examples {
my $template = shift;
return {
simple => {
order => 1,
probes => <<DOC,
+FPing
binary = /usr/bin/fping
DOC
targets => <<DOC,
probe = FPing
menu = Top
title = Network Latency Grapher
remark = Welcome to this SmokePing website.
+ mysite1
menu = Site 1
title = Hosts in Site 1
++ myhost1
host = myhost1.mysite1.example
++ myhost2
host = myhost2.mysite1.example
+ mysite2
menu = Site 2
title = Hosts in Site 2
++ myhost3
host = myhost3.mysite2.example
++ myhost4
host = myhost4.mysite2.example
DOC
probedoc => <<DOC,
Here we have just one probe, fping, pinging four hosts.
The fping probe is using the default parameters, some of them supplied
from the Database section ("step" and "pings"), and some of them by
the probe module.
DOC
targetdoc => <<DOC,
The hosts are located in two sites of two hosts each, and the
configuration has been divided to site sections ('+') and host subsections
('++') accordingly.
DOC
}, # simple
"multiple-probes" => {
order => 2,
probes => <<DOC,
+ FPing
binary = /usr/bin/fping
packetsize = 1000
+ DNS
binary = /usr/bin/dig
lookup = name.example
pings = 5
step = 180
+ EchoPingHttp
pings = 5
url = /test-url
DOC
targets => <<DOC,
probe = FPing
menu = Top
title = Network Latency Grapher
remark = Welcome to this SmokePing website.
+ network
menu = Net latency
title = Network latency (ICMP pings)
++ myhost1
host = myhost1.example
++ myhost2
host = myhost2.example
+ services
menu = Service latency
title = Service latency (DNS, HTTP)
++ DNS
probe = DNS
menu = DNS latency
title = Service latency (DNS)
+++ dns1
host = dns1.example
+++ dns2
host = dns2.example
++ HTTP
menu = HTTP latency
title = Service latency (HTTP)
+++ www1
host = www1.example
+++ www2
host = www2.example
DOC
probedoc => <<DOC,
Here we have three probes: FPing for the regular ICMP pings,
DNS for name server latency measurement and EchoPingHttp
for web servers.
The FPing probe runs with the default parameters, except that the ICMP
packet size is 1000 bytes instead of the default 56 bytes.
The DNS and EchoPingHttp probes have been configured to be a bit more
gentle with the servers, as they only do 5 queries (pings) instead of the
default 20 (or whatever is specified in the Database section). However,
DNS queries are made more often: 5 queries every 3 minutes instead of
every 5 minutes.
DOC
targetdoc => <<DOC,
The target tree has been divided by the probe used. This does not have
to be the case: every target (sub)section can use a different probe,
and the same probe can be used in different parts of the config tree.
DOC
}, # multiple-probes
"fping-instances" => {
order => 3,
probes => <<DOC,
+ FPing
binary = /usr/bin/fping
++ FPingNormal
offset = 0%
++ FPingLarge
packetsize = 5000
offset = 50%
DOC
probedoc => <<DOC,
This example demonstrates the concept of probe instances. The FPingLarge
and FPingNormal probes are independent of each other, they just use
the same module, FPing. FPingNormal uses the default parameters, and
so does FPingLarge except for the 5 kilobyte packetsize. Both use the
same fping binary, and its path is configured FPing top section.
The 'offset' parameters make sure the probes don't run at the same time -
FPingNormal is run every 'full' 5 minutes (eg. 8:00, 8:05, 8:10 and so on,
in wallclock time) while FPingLarge is run halfway through these intervals
(eg. 8:02:30, 8:07:30 etc.)
The top FPing section does not define a probe in itself because it
has subsections. If we really wanted to have one probe named "FPing",
we could do so by making a subsection by that name.
DOC
targets => <<DOC,
probe = FPingNormal
menu = Top
title = Network Latency Grapher
remark = Welcome to this SmokePing website.
+ network
menu = Net latency
title = Network latency (ICMP pings)
++ myhost1
menu = myhost1
title = ICMP latency for myhost1
+++ normal
title = Normal packetsize (56 bytes)
probe = FPingNormal
host = myhost1.example
+++ large
title = Large packetsize (5000 bytes)
probe = FPingLarge
host = myhost1.example
++ myhost2
menu = myhost2
title = ICMP latency for myhost2
+++ normal
title = Normal packetsize (56 bytes)
probe = FPingNormal
host = myhost2.example
+++ large
title = Large packetsize (5000 bytes)
probe = FPingLarge
host = myhost2.example
DOC
targetdoc => <<DOC,
The target section shows two host, myhost1.example and myhost2.example,
being pinged with two differently sized ICMP packets. This time the tree
is divided by the target host rather than the probe.
DOC
}, # fping-instances
"targetvars-with-Curl" => {
order => 4,
probes => <<DOC,
+ Curl
# probe-specific variables
binary = /usr/bin/curl
step = 60
# a default for this target-specific variable
urlformat = http://%host%/
DOC
probedoc => <<DOC,
This example explains the difference between probe- and target-specific
variables. We use the Curl probe for this.
Every probe supports at least some probe-specific variables. The values
of these variables are common to all the targets of the probe, and
they can only be configured in the Probes section. In this case,
the probe-specific variables are "binary" and "step".
Target-specific variables are supported by most probes, the most notable
exception being the FPing probe and its derivatives. Target-specific
variables can have different values for different targets. They can be
configured in both Probes and Targets sections. The values assigned in the
Probes section function become default values that can be overridden
in the Targets section.
The documentation of each probe states which of its variables are
probe-specific and which are target-specific.
In this case the "urlformat" variable is a target-specific one. It is
also quite uncommon, because it can contain a placeholder for the "host"
variable in the Targets section. This is not a general feature, its
usage is only limited to the "urlformat" variable and the "%host%" escape.
(The reason why the FPing probe does not support target-specific variables
is simply the fact that the fping program measures all its targets in one
go, so they all have the same parameters. The other probes ping their targets
one at a time.)
DOC
targets => <<DOC,
probe = Curl
menu = Top
title = Network Latency Grapher
remark = Welcome to this SmokePing website.
+ HTTP
menu = http
title = HTTP latency
++ myhost1
menu = myhost1
title = HTTP latency for myhost1
host = myhost1.example
++ myhost2
menu = myhost2
title = HTTP latency for myhost2
host = myhost2.example
++ myhost3
menu = myhost3
title = HTTP latency for myhost3 (port 8080!)
host = myhost3.example
urlformat = http://%host%:8080/
+ FTP
menu = ftp
title = FTP latency
urlformat = ftp://%host%/
++ myhost1
menu = myhost1
title = FTP latency for myhost1
host = myhost1.example
++ myhost2
menu = myhost2
title = FTP latency for myhost2
host = myhost2.example
DOC
targetdoc => <<DOC,
The target tree is divided into an HTTP branch and an FTP one.
The servers "myhost1.example" and "myhost2.example" are probed
in both. The third server, "myhost3.example", only has an HTTP
server, and it's in a non-standard port (8080).
The "urlformat" variable is specified for the whole FTP branch
as "ftp://%host%/". For the HTTP branch, the default from the
Probes section is used, except for myhost3, which overrides
it to tag the port number into the URL.
The myhost3 assignment could just as well have included the hostname
verbatim (ie. urlformat = http://myhost3.example:8080/) instead of
using the %host% placeholder, but the host variable would still have
been required (even though it wouldn't have been used for anything).
DOC
}, # targetvars-with-Curl
echoping => {
order => 5,
probes => <<DOC,
+ FPing
binary = /usr/bin/fping
# these expect to find echoping in /usr/bin
# if not, you'll have to specify the location separately for each probe
# + EchoPing # uses TCP or UDP echo (port 7)
# + EchoPingDiscard # uses TCP or UDP discard (port 9)
# + EchoPingChargen # uses TCP chargen (port 19)
+ EchoPingSmtp # SMTP (25/tcp) for mail servers
+ EchoPingHttps # HTTPS (443/tcp) for web servers
+ EchoPingHttp # HTTP (80/tcp) for web servers and caches
+ EchoPingIcp # ICP (3130/udp) for caches
# these need at least echoping 6 with the corresponding plugins
+ EchoPingDNS # DNS (53/udp or tcp) servers
+ EchoPingLDAP # LDAP (389/tcp) servers
+ EchoPingWhois # Whois (43/tcp) servers
DOC
probedoc => <<DOC,
This example shows most of the echoping-derived probes in action.
DOC
targets => <<DOC,
# default probe
probe = FPing
menu = Top
title = Network Latency Grapher
remark = Welcome to this SmokePing website.
+ MyServers
menu = My Servers
title = My Servers
++ www-server
menu = www-server
title = Web Server (www-server) / ICMP
# probe = FPing propagated from top
host = www-server.example
+++ http
menu = http
title = Web Server (www-server) / HTTP
probe = EchoPingHttp
host = www-server.example
# default url is /
+++ https
menu = https
title = Web Server (www-server) / HTTPS
probe = EchoPingHttps
host = www-server.example
++ cache
menu = www-cache
title = Web Cache (www-cache) / ICMP
host = www-cache.example
+++ http
menu = http
title = www-cache / HTTP
probe = EchoPingHttp
host = www-cache.example
port = 8080 # use the squid port
url = http://www.somehost.example/
+++ icp
menu = icp
title = www-cache / ICP
probe = EchoPingIcp
host = www-cache.example
url = http://www.somehost.example/
++ mail
menu = mail-server
title = Mail Server (mail-server) / ICMP
host = mail-server.example
+++ smtp
menu = mail-server / SMTP
title = Mail Server (mail-server) / SMTP
probe = EchoPingSmtp
host = mail-server.example
++ ldap-server
menu = ldap-server
title = ldap-server / ICMP
host = ldap-server.example
+++ ldap
menu = ldap-server / LDAP
title = LDAP Server (ldap-server) / LDAP
probe = EchoPingLDAP
ldap_request = (objectclass=*)
host = ldap-server.example
++ name-server
menu = name-server
title = name-server / ICMP
host = name-server.example
+++ DNS
menu = name-server / DNS
title = DNS Server (name-server) / DNS
probe = EchoPingDNS
dns_request = name.example
host = name-server.example
++ whois-server
menu = whois-server
title = whois-server / ICMP
host = whois-server.example
+++ Whois
menu = whois-server / Whois
title = Whois Server (whois-server) / Whois
probe = EchoPingWhois
whois_request = domain.example
host = whois-server.example
DOC
targetdoc => <<DOC,
All the servers are pinged both with ICMP (the FPing probe)
and their respective echoping probe. The proxy server, www-cache,
is probed with both HTTP requests and ICP requests for the same
URL.
DOC
}, # echoping
template => {
order => 6, # last
probes => $template->{probes},
targets => $template->{targets},
probedoc => <<DOC,
This is the template configuration file distributed with Smokeping.
It is included in the examples as well for the sake of completeness.
DOC
targetdoc => <<DOC,
This is the template configuration file distributed with Smokeping.
It is included in the examples as well for the sake of completeness.
DOC
},
}; # return
} # sub examples
1;

View File

@@ -0,0 +1,403 @@
# -*- perl -*-
package Smokeping::Graphs;
use strict;
use Smokeping;
=head1 NAME
Smokeping::Graphs - Functions used in Smokeping for creating graphs
=head1 OVERVIEW
This module currently only contains the code for generating the 'multi target' graphs.
Code for the other graphs will be moved here too in time.
=head2 IMPLEMENTATION
=head3 get_multi_detail
A version of get_detail for multi host graphs where there is data from
multiple targets shown in one graph. The look of the graph is modeld after
the graphs shown in the overview page, except for the size.
=cut
sub get_colors ($){
my $cfg = shift;
my @colorList = ();
my $colorText = $cfg->{Presentation}{colortext};
my $colorBorder = $cfg->{Presentation}{colorborder};
my $colorBackground = $cfg->{Presentation}{colorbackground};
# If graphborders set to no, and no color override, then return default colors as before
if (($cfg->{Presentation}{graphborders} eq 'no') && !($colorText||$colorBorder||$colorBackground)) {
return '--border', '0',
'--color', 'BACK#ffffff00',
'--color', 'CANVAS#ffffff00';
};
# If there are any overrides, use them
if ($cfg->{Presentation}{graphborders} eq 'no') {
push(@colorList, '--border', '0');
};
if ($colorText) {
push(@colorList, '--color', "FONT#${colorText}");
};
if ($colorBorder) {
push(@colorList, '--color', "FRAME#${colorBorder}");
};
if ($colorBackground) {
push(@colorList, '--color', "SHADEA#${colorBackground}");
push(@colorList, '--color', "SHADEB#${colorBackground}");
push(@colorList, '--color', "BACK#${colorBackground}");
push(@colorList, '--color', "CANVAS#${colorBackground}");
};
if (@colorList) { return @colorList[0..$#colorList] };
# Otherwise use rrdtool defaults
return
}
sub get_multi_detail ($$$$;$){
# a) 's' classic with several static graphs on the page
# b) 'n' navigator mode with one graph. below the graph one can specify the end time
# and the length of the graph.
# c) 'a' ajax mode, generate image based on given url and dump in on stdout
#
my $cfg = shift;
my $q = shift;
my $tree = shift;
my $open = shift;
my $mode = shift || $q->param('displaymode') || 's';
my $phys_open = $open;
if ($tree->{__tree_link}){
$tree=$tree->{__tree_link};
$phys_open = $tree->{__real_path};
}
my @dirs = @{$phys_open};
return "<div>ERROR: ".(join ".", @dirs)." has no probe defined</div>"
unless $tree->{probe};
return "<div>ERROR: ".(join ".", @dirs)." $tree->{probe} is not known</div>"
unless $cfg->{__probes}{$tree->{probe}};
return "<div>ERROR: ".(join ".", @dirs)." ist no multi host</div>"
unless $tree->{host} =~ m|^/|;
return "<div>ERROR: unknown displaymode $mode</div>"
unless $mode =~ /^[snca]$/;
my $dir = "";
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 $page;
my $file = pop @dirs;
my @hosts = split /\s+/, $tree->{host};
my $ProbeDesc;
my $ProbeUnit;
my $imgbase;
my $imghref;
my @tasks;
my %lastheight;
my $max = {};
if ($mode eq 's'){
# in nav mode there is only one graph, so the height calculation
# is not necessary.
$imgbase = $cfg->{General}{imgcache}."/".(join "/", @dirs)."/${file}";
$imghref = $cfg->{General}{imgurl}."/".(join "/", @dirs)."/${file}";
@tasks = @{$cfg->{Presentation}{detail}{_table}};
if (open (HG,"<${imgbase}.maxheight")){
while (<HG>){
chomp;
my @l = split / /;
$lastheight{$l[0]} = $l[1];
}
close HG;
}
for my $rrd (@hosts){
my $newmax = Smokeping::findmax($cfg, $cfg->{General}{datadir}.$rrd.".rrd");
map {$max->{$_} = $newmax->{$_} if not $max->{$_} or $newmax->{$_} > $max->{$_} } keys %{$newmax};
}
if (open (HG,">${imgbase}.maxheight")){
foreach my $size (keys %{$max}){
print HG "$size $max->{$size}\n";
}
close HG;
}
}
elsif ($mode eq 'n' or $mode eq 'a') {
if ($mode eq 'n') {
$imgbase =$cfg->{General}{imgcache}."/__navcache/".time()."$$";
$imghref =$cfg->{General}{imgurl}."/__navcache/".time()."$$";
} else {
my $serial = int(rand(2000));
$imgbase =$cfg->{General}{imgcache}."/__navcache/".$serial;
$imghref =$cfg->{General}{imgurl}."/__navcache/".$serial;
}
mkdir $cfg->{General}{imgcache}."/__navcache",0755 unless -d $cfg->{General}{imgcache}."/__navcache";
# remove old images after one hour
my $pattern = $cfg->{General}{imgcache}."/__navcache/*.png";
for (glob $pattern){
unlink $_ if time - (stat $_)[9] > 3600;
}
@tasks = (["Navigator Graph", Smokeping::parse_datetime($q->param('start')),Smokeping::parse_datetime($q->param('end'))]);
} else {
# chart mode
mkdir $cfg->{General}{imgcache}."/__chartscache",0755 unless -d $cfg->{General}{imgcache}."/__chartscache";
# remove old images after one hour
my $pattern = $cfg->{General}{imgcache}."/__chartscache/*.png";
for (glob $pattern){
unlink $_ if time - (stat $_)[9] > 3600;
}
my $desc = join "/",@{$open};
@tasks = ([$desc , time()-3600, time()]);
$imgbase = $cfg->{General}{imgcache}."/__chartscache/".(join ".", @dirs).".${file}";
$imghref = $cfg->{General}{imgurl}."/__chartscache/".(join ".", @dirs).".${file}";
}
if ($mode =~ /[anc]/){
my $val = 0;
for my $host (@hosts){
my ($graphret,$xs,$ys) = RRDs::graph
("dummy",
'--start', $tasks[0][1],
'--end', $tasks[0][2],
"DEF:maxping=$cfg->{General}{datadir}${host}.rrd:median:AVERAGE",
'PRINT:maxping:MAX:%le' );
my $ERROR = RRDs::error();
return "<div>RRDtool did not understand your input: $ERROR.</div>" if $ERROR;
$val = $graphret->[0] if $val < $graphret->[0];
}
$val = 1e-6 if $val =~ /nan/i;
$max = { $tasks[0][1] => $val * 1.5 };
}
for (@tasks) {
my ($desc,$start,$end) = @{$_};
my $xs;
my $ys;
my $sigtime = ($end and $end =~ /^\d+$/) ? $end : time;
my $date = $cfg->{Presentation}{detail}{strftime} ?
POSIX::strftime($cfg->{Presentation}{detail}{strftime}, localtime($sigtime)) : scalar localtime($sigtime);
if ( $RRDs::VERSION >= 1.199908 ){
$date =~ s|:|\\:|g;
}
$end ||= 'last';
$start = Smokeping::exp2seconds($start) if $mode =~ /[s]/;
my $startstr = $start =~ /^\d+$/ ? POSIX::strftime("%Y-%m-%d %H:%M",localtime($mode eq 'n' ? $start : time-$start)) : $start;
my $endstr = $end =~ /^\d+$/ ? POSIX::strftime("%Y-%m-%d %H:%M",localtime($mode eq 'n' ? $end : time)) : $end;
my $realstart = ( $mode =~ /[sc]/ ? '-'.$start : $start);
my @G;
my @colors = split /\s+/, $cfg->{Presentation}{multihost}{colors};
my $i = 0;
for my $host (@hosts){
$i++;
my $swidth = $max->{$start} / $cfg->{Presentation}{detail}{height};
my $rrd = $cfg->{General}{datadir}.$host.".rrd";
next unless -r $rrd; # skip things that do not exist;
my $medc = shift @colors;
my @tree_path = split /\//,$host;
shift @tree_path;
my ($host,$real_slave) = split /~/, $tree_path[-1]; #/
$tree_path[-1] = $host;
my $tree = Smokeping::get_tree($cfg,\@tree_path);
my $label = $tree->{menu};
if ($real_slave){
$label .= "<". $cfg->{Slaves}{$real_slave}{display_name};
}
my $probe = $cfg->{__probes}{$tree->{probe}};
my $XProbeDesc = $probe->ProbeDesc();
if (not $ProbeDesc or $ProbeDesc eq $XProbeDesc){
$ProbeDesc = $XProbeDesc;
}
else {
$ProbeDesc = "various probes";
}
my $XProbeUnit = $probe->ProbeUnit();
if (not $ProbeUnit or $ProbeUnit eq $XProbeUnit){
$ProbeUnit = $XProbeUnit;
}
else {
$ProbeUnit = "various units";
}
my $pings = $probe->_pings($tree);
$label = sprintf("%-20s",$label);
$label =~ s/:/\\:/g;
push @colors, $medc;
my $sdc = $medc;
my $stddev = Smokeping::RRDhelpers::get_stddev($rrd,'median','AVERAGE',$realstart,$sigtime) || 0;
$sdc =~ s/^(......).*/${1}30/;
push @G,
"DEF:median$i=${rrd}:median:AVERAGE",
"DEF:loss$i=${rrd}:loss:AVERAGE",
"CDEF:ploss$i=loss$i,$pings,/,100,*",
"CDEF:dm$i=median$i,0,".$max->{$start}.",LIMIT",
Smokeping::calc_stddev($rrd,$i,$pings),
"CDEF:dmlow$i=dm$i,sdev$i,2,/,-",
"CDEF:s2d$i=sdev$i",
# "CDEF:dm2=median,1.5,*,0,$max,LIMIT",
# "LINE1:dm2", # this is for kicking things down a bit
"AREA:dmlow$i",
"AREA:s2d${i}#${sdc}::STACK",
"LINE1:dm$i#${medc}:${label}",
"VDEF:avmed$i=median$i,AVERAGE",
"VDEF:avsd$i=sdev$i,AVERAGE",
"CDEF:msr$i=median$i,POP,avmed$i,avsd$i,/",
"VDEF:avmsr$i=msr$i,AVERAGE",
"GPRINT:avmed$i:%5.1lf %ss av md ",
"GPRINT:ploss$i:AVERAGE:%5.1lf %% av ls",
sprintf('COMMENT:%5.1lf ms sd',$stddev*1000.0),
"GPRINT:avmsr$i:%5.1lf %s am/as\\l";
};
my @task;
push @task, "--logarithmic" if $cfg->{Presentation}{detail}{logarithmic} and
$cfg->{Presentation}{detail}{logarithmic} eq 'yes';
push @task, '--lazy' if $mode eq 's' and $lastheight{$start} == $max->{$start};
push @task,
"${imgbase}_${end}_${start}.png",
'--start',$realstart,
($end ne 'last' ? ('--end',$end) : ()),
'--height',$cfg->{Presentation}{detail}{height},
'--width',$cfg->{Presentation}{detail}{width},
'--title',$cfg->{Presentation}{htmltitle} ne 'yes' ? $desc : '',
'--rigid','--upper-limit', $max->{$start},
'--lower-limit',($cfg->{Presentation}{detail}{logarithmic} ? ($max->{$start} > 0.01) ? '0.001' : '0.0001' : '0'),
'--vertical-label',$ProbeUnit,
'--imgformat','PNG',
Smokeping::Graphs::get_colors($cfg),
@G,
"COMMENT:$ProbeDesc",
"COMMENT:$date\\j";
my $graphret;
($graphret,$xs,$ys) = RRDs::graph @task;
# print "<div>INFO:".join("<br/>",@task)."</div>";
my $ERROR = RRDs::error();
if ($ERROR) {
return "<div>ERROR: $ERROR</div><div>".join("<br/>",@task)."</div>";
};
if ($mode eq 'a'){ # ajax mode
open my $img, "${imgbase}_${end}_${start}.png";
binmode $img;
print "Content-Type: image/png\n";
my $data;
read($img,$data,(stat($img))[7]);
close $img;
print "Content-Length: ".length($data)."\n\n";
print $data;
unlink "${imgbase}_${end}_${start}.png";
return undef;
}
elsif ($mode eq 'n'){ # navigator mode
$page .= "<div class=\"panel\">";
$page .= "<div class=\"panel-heading\"><h2>$desc</h2></div>"
if $cfg->{Presentation}{htmltitle} eq 'yes';
$page .= "<div class=\"panel-body\">";
$page .= qq|<IMG id="zoom" alt="" width="$xs" height="$ys" SRC="${imghref}_${end}_${start}.png">| ;
$page .= $q->start_form(-method=>'GET', -id=>'range_form')
. "<p>Time range: "
. $q->textfield(-name=>'start',-default=>$startstr)
. "&nbsp;&nbsp;to&nbsp;&nbsp;".$q->textfield(-name=>'end',-default=>$endstr)
. $q->hidden(-name=>'epoch_start',-id=>'epoch_start',-default=>$start)
. $q->hidden(-name=>'epoch_end',-id=>'epoch_end',-default=>time())
. $q->hidden(-name=>'target',-id=>'target' )
. $q->hidden(-name=>'hierarchy',-id=>'hierarchy' )
. $q->hidden(-name=>'displaymode',-default=>$mode )
. "&nbsp;"
. $q->submit(-name=>'Generate!')
. "</p>"
. $q->end_form();
$page .= "</div></div>\n";
} elsif ($mode eq 's') { # classic mode
$startstr =~ s/\s/%20/g;
$endstr =~ s/\s/%20/g;
$page .= "<div class=\"panel\">";
# $page .= (time-$timer_start)."<br/>";
# $page .= join " ",map {"'$_'"} @task;
$page .= "<div class=\"panel-heading\"><h2>$desc</h2></div>"
if $cfg->{Presentation}{htmltitle} eq 'yes';
$page .= "<div class=\"panel-body\">";
$page .= ( qq{<a href="?displaymode=n;start=$startstr;end=now;}."target=".$q->param('target').'">'
. qq{<IMG ALT="" SRC="${imghref}_${end}_${start}.png" class="img-responsive">}."</a>" ); #"
$page .= "</div></div>\n";
} else { # chart mode
$page .= "<div class=\"panel\">";
$page .= "<div class=\"panel-heading\"><h2>$desc</h2></div>"
if $cfg->{Presentation}{htmltitle} eq 'yes';
$page .= "<div class=\"panel-body\">";
$page .= ( qq{<a href="}.lnk($q, (join ".", @$open)).qq{">}
. qq{<IMG ALT="" SRC="${imghref}_${end}_${start}.png" class="img-responsive">}."</a>" ); #"
$page .= "</div></div>\n";
}
}
return $page;
}
1;
__END__
=head1 COPYRIGHT
Copyright 2007 by Tobias Oetiker
=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

View File

@@ -0,0 +1,204 @@
# -*- perl -*-
package Smokeping::Info;
use warnings;
use strict;
use RRDs;
use Smokeping;
use Carp;
use Data::Dumper;
sub new {
my $this = shift;
my $class = ref($this) || $this;
my $self = { cfg_file => shift };
bless $self, $class;
my $parser = Smokeping::get_parser();
$self->{cfg_hash} = $parser->parse( $self->{cfg_file} )
or croak "ERROR reading config file $parser->{err}";
$self->{probe_hash} = Smokeping::load_probes $self->{cfg_hash};
return $self;
}
# get a list of all rrd files in the config file
sub __flatten_targets;
sub __flatten_targets {
my $probes = shift;
my $root = shift;
my $prefix = shift;
my @paths;
for my $target ( sort {$root->{$a}{_order} <=> $root->{$b}{_order}}
grep { ref $root->{$_} eq 'HASH' } keys %$root ) {
push @paths, __flatten_targets($probes,$root->{$target},$prefix.'/'.$target);
};
if (exists $root->{host} and not $root->{host} =~ m|/|){
my $probe = $probes->{$root->{probe}};
my $pings = $probe->_pings($root);
if (not $root->{nomasterpoll} or $root->{nomasterpoll} eq 'no') {
push @paths, { path => $prefix, pings=>$pings };
};
if ($root->{slaves}) {
for my $slave (split /\s+/,$root->{slaves}){
push @paths, { path => $prefix.'~'.$slave, pings=>$pings };
}
}
};
return @paths;
}
sub fetch_nodes {
my $self = shift;
my %args = ( 'mode' => 'plain', @_); # no mode is default
my %valid = ( pattern=>1, mode => 1 );
my %valid_modes = ( plain=>1, recursive=>1, regexp=>1);
map {
croak "Invalid fetch nodes argument '$_'"
if not $valid{$_};
} keys %args;
croak "Invalid fetch mode $args{mode}"
if not $valid_modes{$args{mode}};
my $cfg = $self->{cfg_hash};
my @flat = __flatten_targets($self->{probe_hash},$cfg->{Targets},'');
my $rx = qr{.*};
if ( defined $args{pattern} ) {
if ( $args{mode} eq 'recursive' ) {
$rx = qr{^\Q$args{pattern}\E};
}
elsif ( $args{mode} eq 'regexp' ) {
$rx = qr{$args{pattern}};
}
else {
$rx = qr{^\Q$args{pattern}\E[^/]*$};
}
}
return [ grep { $_->{path} =~ /${rx}/ } @flat ];
}
sub stat_node {
my $self = shift;
my $path = shift;
my $start = shift;
my $end = shift;
my $cfg = $self->{cfg_hash};
my ($graphret,$xs,$ys) = RRDs::graph (
'/tmp/dummy',
'--start'=>$start,
'--end'=>$end,
'DEF:loss_avg_r='.$cfg->{General}{datadir}.$path->{path}.'.rrd:loss:AVERAGE',
'CDEF:loss_avg=loss_avg_r,'.$path->{pings}.',/',
'VDEF:loss_avg_tot=loss_avg,AVERAGE',
'PRINT:loss_avg_tot:%.8le',
'DEF:loss_max_r='.$cfg->{General}{datadir}.$path->{path}.'.rrd:loss:MAX',
'CDEF:loss_max=loss_max_r,'.$path->{pings}.',/',
'VDEF:loss_max_tot=loss_max,MAXIMUM',
'PRINT:loss_max_tot:%.8le',
'VDEF:loss_now=loss_avg,LAST',
'PRINT:loss_now:%.8le',
'DEF:median_avg='.$cfg->{General}{datadir}.$path->{path}.'.rrd:median:AVERAGE',
'VDEF:median_avg_tot=median_avg,AVERAGE',
'PRINT:median_avg_tot:%.8le',
'DEF:median_min='.$cfg->{General}{datadir}.$path->{path}.'.rrd:median:MIN',
'VDEF:median_min_tot=median_min,MINIMUM',
'PRINT:median_min_tot:%.8le',
'DEF:median_max='.$cfg->{General}{datadir}.$path->{path}.'.rrd:median:MAX',
'VDEF:median_max_tot=median_max,MAXIMUM',
'PRINT:median_max_tot:%.8le',
'VDEF:median_now=median_avg,LAST',
'PRINT:median_now:%.8le'
);
my %data;
if (my $ERROR = RRDs::error()){
carp "$path->{path}: $ERROR";
} else {
@data{qw(loss_avg loss_max loss_now med_avg med_min med_max med_now)} = @$graphret;
}
return \%data;
};
1;
__END__
=head1 NAME
Smokeping::Info - Pull numerical info out of the rrd databases
=head1 OVERVIEW
This module provides methods to further process information contained in
smokeping rrd files. The smokeinfo tool is a simple wrapper around the
functionality contained in here.
my $si = Smokeping::Info->new("config/file/path");
my $array_ref = $si->fetch_nodes(pattern=>'/node/path',
mode=>'recursive');
my $hash_ref = $si->stat_node(path,start,end);
=head1 IMPLEMENTATION
=head2 new(path)
Create a new Smokeping::Info instance. Instantiating Smokeping::Info entails
reading the configuration file. This is a compute heavy procedure. So you may
want to use a single info object to handle multiple requests.
=head2 fetch_nodes(pattern=>'/...',mode=>{recursive|regexp})
The fetch_nodes method will find all nodes sitting in the given pattern
(absolute path) including the path itself. By setting the recursive mode,
all rrd files in paths below will be returned as well. In regexp mode, all
rrd paths matching the given expression will be returned.
=head2 stat_node(node,start,end)
Return a hash pointer to statistics based on the data stored in the given
rrd path.
med_avg - average median
med_min - minimal median
med_max - maximal median
med_now - current median
loss_avg - average loss
loss_max - maximum loss
loss_now - current loss
=head1 COPYRIGHT
Copyright 2009 by OETIKER+PARTNER AG
=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>, development sponsored by Swisscom Hospitality
=cut
# Emacs Configuration
#
# Local Variables:
# mode: cperl
# eval: (cperl-set-style "PerlStyle")
# mode: flyspell
# mode: flyspell-prog
# End:
#
# vi: sw=4

View File

@@ -0,0 +1,339 @@
# -*- perl -*-
package Smokeping::Master;
use Data::Dumper;
use Storable qw(nstore dclone fd_retrieve);
use strict;
use warnings;
use Fcntl qw(:flock);
use Digest::HMAC_MD5 qw(hmac_md5_hex);
use File::Basename qw(dirname);
use File::Path qw(make_path);
# keep this in sync with the Slave.pm part
# only update if you have to force a parallel upgrade
my $PROTOCOL = "2";
=head1 NAME
Smokeping::Master - Master Functionality for Smokeping
=head1 OVERVIEW
This module handles all special functionality required by smokeping running
in master mode.
=head2 IMPLEMENTATION
=head3 slave_cfg=extract_config(cfg,slave)
Extract the relevant configuration information for the selected slave. The
configuration will only contain the information that is relevant for the
slave. Any parameters overwritten in the B<Slaves> section of the configuration
file will be patched for the slave.
=cut
sub get_targets;
sub get_targets {
my $trg = shift;
my $slave = shift;
my %return;
my $ok;
foreach my $key (keys %{$trg}){
# dynamic hosts can only be queried from the
# master
next if $key eq 'host' and $trg->{$key} eq 'DYNAMIC';
next if $key eq 'host' and $trg->{$key} =~ m|^/|; # skip multi targets
next if $key eq 'host' and not ( defined $trg->{slaves} and $trg->{slaves} =~ /\b${slave}\b/);
if (ref $trg->{$key} eq 'HASH'){
$return{$key} = get_targets ($trg->{$key},$slave);
$ok = 1 if defined $return{$key};
} else {
$ok = 1 if $key eq 'host';
$return{$key} = $trg->{$key};
}
}
$return{nomasterpoll} = 'no'; # slaves poll always
return ($ok ? \%return : undef);
}
sub extract_config {
my $cfg = shift;
my $slave = shift;
# get relevant Targets
my %slave_config;
$slave_config{Database} = dclone $cfg->{Database};
$slave_config{General} = dclone $cfg->{General};
$slave_config{Probes} = dclone $cfg->{Probes};
$slave_config{Targets} = get_targets($cfg->{Targets},$slave);
$slave_config{__last} = $cfg->{__last};
if ($cfg->{Slaves} and $cfg->{Slaves}{$slave} and $cfg->{Slaves}{$slave}{override}){
for my $override (keys %{$cfg->{Slaves}{$slave}{override}}){
my $node = \%slave_config;
my @keys = split /\./, $override;
my $last_key = pop @keys;
for my $key (@keys){
$node->{$key} = {}
unless $node->{$key} and ref $node->{$key} eq 'HASH';
$node = $node->{$key};
}
$node->{$last_key} = $cfg->{Slaves}{$slave}{override}{$override};
}
}
if ($slave_config{Targets}){
return Dumper \%slave_config;
} else {
return undef;
}
}
=head3 save_updates (updates)
When the cgi gets updates from a client, these updates are saved away, for
each 'target' so that the updates can be integrated into the relevant rrd
database by the rrd daemon as the next round of updates is processed. This
two stage process is chosen so that all results flow through the same code
path in the daemon.
The updates are stored in the directory configured as 'dyndir' in the 'General'
configuration section, defaulting to the value of 'datadir' from the same section
if 'dyndir' is not present.
=cut
sub slavedatadir ($) {
my $cfg = shift;
my $dir = $cfg->{General}{dyndir} ||
$cfg->{General}{datadir};
$dir =~ s{/*$}{};
return $dir;
}
sub make_slavedatadir ($) {
my $file = shift;
my $dir = dirname($file);
if (! -d $dir) {
make_path($dir, {'error' => \my $err});
if ($err && @$err) {
for my $diag (@$err) {
my ($f,$m) = %$diag;
warn "Failed to create slave cache directory [$f]: $m";
}
} else {
warn "Slave cache directory $dir created\n";
}
}
}
sub save_updates {
my $cfg = shift;
my $slave = shift;
my $updates = shift;
# name\ttime\tupdatestring
# name\ttime\tupdatestring
my %u;
for my $update (split /\n/, $updates){
my ($name, $time, $updatestring) = split /\t/, $update;
if ( ${name} =~ m{(^|/)\.\.($|/)} ){
warn "Skipping update for ${name}.${slave}.slave_cache since ".
"you seem to try todo some directory magic here. Don't!";
} else {
push @{$u{$name}}, [$time,$updatestring];
}
}
for my $name (sort keys %u){
my $file = slavedatadir($cfg) ."/${name}.${slave}.slave_cache";
for (my $i = 2; $i >= 0; $i--){
my $fh;
make_slavedatadir($file);
if ( open ($fh, '+>>' , $file) and flock($fh, LOCK_EX) ){
my $existing = [];
if (! -e $file) { # the reader unlinked it from under us
flock($fh, LOCK_UN);
close $fh;
next;
}
seek $fh, 0, 0;
if ( -s _ ){
my $in = eval { fd_retrieve $fh };
if ($@) { #error
warn "Loading $file: $@";
} else {
$existing = $in;
};
};
map {
push @{$existing}, [ $slave, $_->[0], $_->[1] ];
} @{$u{$name}};
nstore($existing, $file.$$);
rename $file.$$,$file;
flock($fh, LOCK_UN);
close $fh;
last;
} elsif ($i > 0) {
warn "Could not lock $file ($!). Trying again $i more times.\n";
sleep rand(2);
next;
}
warn "Could not update $file, giving up for now.";
close $fh;
}
}
};
=head3 get_slaveupdates
Read in all updates provided by the selected slave and return an array reference.
=cut
sub get_slaveupdates {
my $cfg = shift;
my $name = shift;
my $slave = shift;
my $file = $name . "." . $slave. ".slave_cache";
my $empty = [];
my $data;
my $datadir = $cfg->{General}{datadir};
my $dir = slavedatadir($cfg);
$file =~ s/^\Q$datadir\E/$dir/;
my $fh;
if ( open ($fh, '<', $file) ) {
if ( flock $fh, LOCK_SH ){
eval { $data = fd_retrieve $fh };
unlink $file;
flock $fh, LOCK_UN;
if ($@) { #error
warn "Loading $file: $@";
close $fh;
return $empty;
}
} else {
warn "Could not lock $file. Will skip and try again in the next round. No harm done!\n";
}
close $fh;
return $data;
}
return $empty;
}
=head3 get_secret
Read the secrets file and figure the secret for the slave which is talking to us.
=cut
sub get_secret {
my $cfg = shift;
my $slave = shift;
if (open my $hand, "<", $cfg->{Slaves}{secrets}){
while (<$hand>){
next unless /^${slave}:(\S+)/;
close $hand;
return $1;
}
} else {
print "Content-Type: text/plain\n\n";
print "WARNING: Opening secrets file $cfg->{Slaves}{secrets}: $!\n";
return '__HORRIBLE_INLINE_SIGNALING__';
}
return;
}
=head3 answer_slave
Answer the requests from the slave by accepting the data, verifying the secrets
and providing updated config information if necessary.
=cut
sub answer_slave {
my $cfg = shift;
my $q = shift;
my $slave = $q->param('slave');
my $secret = get_secret($cfg,$slave);
return if $secret eq '__HORRIBLE_INLINE_SIGNALING__';
if (not $secret){
print "Content-Type: text/plain\n\n";
print "WARNING: No secret found for slave ${slave}\n";
return;
}
my $protocol = $q->param('protocol') || '?';
if (not $protocol eq $PROTOCOL){
print "Content-Type: text/plain\n\n";
print "WARNING: I expected protocol $PROTOCOL and got $protocol from slave ${slave}. I will skip this.\n";
return;
}
my $key = $q->param('key');
my $data = $q->param('data');
my $config_time = $q->param('config_time');
if (not ref $cfg->{Slaves}{$slave} eq 'HASH'){
print "Content-Type: text/plain\n\n";
print "WARNING: I don't know the slave ${slave} ignoring it";
return;
}
# lets make sure the we share a secret
if (hmac_md5_hex($data,$secret) eq $key){
save_updates $cfg, $slave, $data;
} else {
print "Content-Type: text/plain\n\n";
print "WARNING: Data from $slave was signed with $key which does not match our expectation\n";
return;
}
# does the client need new config ?
if ($config_time < $cfg->{__last}){
my $config = extract_config $cfg, $slave;
if ($config){
print "Content-Type: application/smokeping-config\n";
print "Protocol: $PROTOCOL\n";
print "Key: ".hmac_md5_hex($config,$secret)."\n\n";
print $config;
} else {
print "Content-Type: text/plain\n\n";
print "WARNING: No targets found for slave '$slave'\n";
return;
}
} else {
print "Content-Type: text/plain\n\nOK\n";
};
return;
}
1;
__END__
=head1 COPYRIGHT
Copyright 2007 by Tobias Oetiker
=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

View File

@@ -0,0 +1,92 @@
# -*- perl -*-
package Smokeping::RRDhelpers;
=head1 NAME
Smokeping::RRDhelpers - Functions for doing 'interesting things' with RRDs.
=head1 OVERVIEW
This module holds a collection of functions for doing advanced calculations
and effects on rrd files.
=cut
use strict;
use RRDs;
=head2 IMPLEMENTATION
=head3 get_stddev(rrd,ds,cf,start,end[,step])
Pull the data values off the rrd file and calculate the standard deviation. Nan
values get ignored in this process.
=cut
sub get_stddev{
my $rrd = shift;
my $ds = shift;
my $cf = shift;
my $start = shift;
my $end = shift;
my $step = shift;
my ($realstart,$realstep,$names,$array) = RRDs::fetch $rrd, $cf, '--start',$start, '--end',$end,($step ? ('--resolution',$step):());
if (my $err = RRDs::error){
warn $err
};
my $idx = 0;
for (@$names){
last if $ds eq $_;
$idx ++;
}
my $sum = 0;
my $sqsum = 0;
my $cnt = 0;
foreach my $line (@$array){
my $val = $line->[$idx];
if (defined $val){
$cnt++;
$sum += $val;
$sqsum += $val**2;
}
}
return undef unless $cnt;
my $sqdev = 1.0 / $cnt * ( $sqsum - $sum**2 / $cnt );
return $sqdev < 0.0 ? 0.0 : sqrt($sqdev);
}
1;
__END__
=head1 COPYRIGHT
Copyright 2007 by Tobias Oetiker
=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

View File

@@ -0,0 +1,225 @@
package Smokeping::RRDtools;
=head1 NAME
Smokeping::RRDtools - Tools for RRD file handling
=head1 SYNOPSIS
use Smokeping::RRDtools;
use RRDs;
my $file = '/path/to/file.rrd';
# get the create arguments that $file was created with
my $create = Smokeping::RRDtools::info2create($file);
# use them to create a new file
RRDs::create('/path/to/file2.rrd', @$create);
# or compare them against another create list
my @create = ('--step', 60, 'DS:ds0:GAUGE:120:0:U', 'RRA:AVERAGE:0.5:1:1008');
my ($fatal, $comparison) = Smokeping::RRDtools::compare($file, \@create);
print "Fatal: " if $fatal;
print "Create arguments didn't match: $comparison\n" if $comparison;
Smokeping::RRDtools::tuneds($file, \@create);
=head1 DESCRIPTION
This module offers three functions, C<info2create>, C<compare> and
C<tuneds>. The first can be used to recreate the arguments that an RRD file
was created with. The second checks if an RRD file was created with the
given arguments. The thirds tunes the DS parameters according to the
supplied create string.
The function C<info2create> must be called with one argument:
the path to the interesting RRD file. It will return an array
reference of the argument list that can be fed to C<RRDs::create>.
Note that this list will never contain the C<start> parameter,
but it B<will> contain the C<step> parameter.
The function C<compare> must be called with two arguments: the path to the
interesting RRD file, and a reference to an argument list that could be fed
to C<RRDs::create>. The function will then simply compare the result of
C<info2create> with this argument list. It will return an array of two values:
C<(fatal, text)> where C<fatal> is 1 if it found a fatal difference, and 0 if not.
The C<text> will contain an error message if C<fatal == 1> and a possible warning
message if C<fatal == 0>. If C<fatal == 0> and C<text> is C<undef>, all the
arguments matched.
Note that if there is a C<start> parameter in the argument list,
C<compare> disregards it. If C<step> isn't specified, C<compare> will use
the C<rrdtool> default of 300 seconds. C<compare> ignores non-matching DS
parameters since C<tuneds> will fix them.
C<tuneds> talks on stderr about the parameters it fixes.
=head1 NOTES
This module is not particularly specific to Smokeping, it is just
distributed with it.
=head1 BUGS
Probably.
=head1 COPYRIGHT
Copyright (c) 2005 by Niko Tyni.
=head1 AUTHOR
Niko Tyni <ntyni@iki.fi>
=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 SEE ALSO
RRDs(3)
=cut
use strict;
use RRDs;
# take an RRD file and make a create list out of it
sub info2create {
my $file = shift;
my @create;
# check for Perl version 5.8.0, it's buggy
# no more v-strings
my $buggy_perl_version = 1 if abs($] - 5.008000) < .0000005;
my $info = RRDs::info($file);
my $error = RRDs::error;
die("RRDs::info $file: ERROR: $error") if $error;
die("$file: unknown RRD version: $info->{rrd_version}")
unless $info->{rrd_version} eq '0001'
or $info->{rrd_version} eq '0003';
my $cf = $info->{"rra[0].cf"};
die("$file: no RRAs found?")
unless defined $cf;
my @fetch = RRDs::fetch($file, $cf, "-s 0", "-e 0");
$error = RRDs::error;
die("RRDs::fetch $file $cf: ERROR: $error") if $error;
my @ds = @{$fetch[2]};
push @create, '--step', $info->{step};
for my $ds (@ds) {
my @s = ("DS", $ds);
for (qw(type minimal_heartbeat min max)) {
die("$file: missing $_ for DS $ds?")
unless exists $info->{"ds[$ds].$_"}
or $buggy_perl_version;
my $val = $info->{"ds[$ds].$_"};
push @s, defined $val ? $val : "U";
}
push @create, join(":", @s);
}
for (my $i=0; exists $info->{"rra[$i].cf"}; $i++) {
my @s = ("RRA", $info->{"rra[$i].cf"});
for (qw(xff pdp_per_row rows)) {
die("$file: missing $_ for RRA $i")
unless exists $info->{"rra[$i].$_"}
or $buggy_perl_version;
push @s, $info->{"rra[$i].$_"};
}
push @create, join(":", @s);
}
return \@create;
}
sub compare {
my $file = shift;
my $create = shift;
my @create2 = @{info2create($file)};
my @create = @$create; # copy because we change it
# we don't compare the '--start' param
if ($create[0] eq '--start') {
shift @create;
shift @create;
}
# special check for the optional 'step' parameter
die("Internal error: didn't get the step parameter from info2create?")
unless ("--step" eq shift @create2);
my $step = shift @create2;
my $step2;
if ($create[0] eq '--step') {
shift @create;
$step2 = shift @create;
} else {
$step2 = 300; # default value
}
return (1, "Wrong value of step: $file has $step, create string has $step2")
unless $step == $step2;
my $dscount = grep /^DS/, @create;
my $dscount2 = grep /^DS/, @create2;
return (1, "Different number of data sources: $file has $dscount2, create string has $dscount")
unless $dscount == $dscount2;
my $rracount = grep /^RRA/, @create;
my $rracount2 = grep /^RRA/, @create2;
return (1, "Different number of RRAs: $file has $rracount2, create string has $rracount")
unless $rracount == $rracount2;
my $warning;
while (my $arg = shift @create) {
my $arg2 = shift @create2;
my @ds = split /:/, $arg;
my @ds2 = split /:/, $arg2;
next if $ds[0] eq 'DS' and $ds[0] eq $ds2[0] and $ds[1] eq $ds2[1] and $ds[2] eq $ds2[2];
if ($arg ne $arg2) {
if ($ds[0] eq 'RRA' and $ds[0] eq $ds2[0] and $ds[1] eq $ds2[1]) {
# non-fatal: CF is the same, but xff/steps/rows differ
$warning .= "Different RRA parameters: $file has $arg2, create string has $arg";
} else {
return (1, "Different arguments: $file has $arg2, create string has $arg");
}
}
}
return (0, $warning);
}
sub tuneds {
my $file = shift;
my $create = shift;
my @create2 = sort grep /^DS/, @{info2create($file)};
my @create = sort grep /^DS/, @$create;
while (@create){
my @ds = split /:/, shift @create;
my @ds2 = split /:/, shift @create2;
next unless $ds[1] eq $ds2[1] and $ds[2] eq $ds[2];
if ($ds[3] ne $ds2[3]){
warn "## Updating $file DS:$ds[1] heartbeat $ds2[3] -> $ds[3]\n";
RRDs::tune $file,"--heartbeat","$ds[1]:$ds[3]" unless $ds[3] eq $ds2[3];
}
if ($ds[4] ne $ds2[4]){
warn "## Updating $file DS:$ds[1] minimum $ds2[4] -> $ds[4]\n";
RRDs::tune $file,"--minimum","$ds[1]:$ds[4]" unless $ds[4] eq $ds2[4];
}
if ($ds[5] ne $ds2[5]){
warn "## Updating $file DS:$ds[1] maximum $ds2[5] -> $ds[5]\n";
RRDs::tune $file,"--maximum","$ds[1]:$ds[5]" unless $ds[5] eq $ds2[5];
}
}
}
1;

View File

@@ -0,0 +1,167 @@
# -*- perl -*-
package Smokeping::Slave;
use warnings;
use strict;
use Data::Dumper;
use Storable qw(nstore retrieve);
use Digest::HMAC_MD5 qw(hmac_md5_hex);
use LWP::UserAgent;
use Safe;
use Smokeping;
# keep this in sync with the Slave.pm part
# only update if you have to force a parallel upgrade
my $PROTOCOL = "2";
=head1 NAME
Smokeping::Slave - Slave functionality for Smokeping
=head1 OVERVIEW
The Module implements the functionality required to run in slave mode.
=head2 IMPLEMENTATION
=head3 submit_results
In slave mode we just hit our targets and submit the results to the server.
If we can not get to the server, we submit the results in the next round.
The server in turn sends us new config information if it sees that ours is
out of date.
=cut
sub get_results;
sub get_results {
my $slave_cfg = shift;
my $cfg = shift;
my $probes = shift;
my $tree = shift;
my $name = shift;
my $justthisprobe = shift; # if defined, update only the targets probed by this probe
my $probe = $tree->{probe};
my $results = [];
return [] unless $cfg;
foreach my $prop (keys %{$tree}) {
if (ref $tree->{$prop} eq 'HASH'){
my $subres = get_results $slave_cfg, $cfg, $probes, $tree->{$prop}, $name."/$prop", $justthisprobe;
push @{$results}, @{$subres};
}
next unless defined $probe;
next if defined $justthisprobe and $probe ne $justthisprobe;
my $probeobj = $probes->{$probe};
if ($prop eq 'host') {
#print "update $name\n";
my $updatestring = $probeobj->rrdupdate_string($tree);
push @$results, "$name\t".time()."\t$updatestring";
}
}
return $results;
}
sub submit_results {
my $slave_cfg = shift;
my $cfg = shift;
my $myprobe = shift;
my $probes = shift;
my $store = $slave_cfg->{cache_dir}."/data";
$store .= "_$myprobe" if $myprobe;
$store .= ".cache";
my $restore = -f $store ? retrieve $store : [];
unlink $store;
my $new = get_results($slave_cfg, $cfg, $probes, $cfg->{Targets}, '', $myprobe);
push @$restore, @$new;
my $data_dump = join("\n",@{$restore}) || "";
my $ua = LWP::UserAgent->new(
agent => 'smokeping-slave/1.0',
timeout => 60,
env_proxy => 1 );
my $response = $ua->post(
$slave_cfg->{master_url},
Content_Type => 'form-data',
Content => [
slave => $slave_cfg->{slave_name},
key => hmac_md5_hex($data_dump,$slave_cfg->{shared_secret}),
protocol => $PROTOCOL,
data => $data_dump,
config_time => $cfg->{__last} || 0,
],
);
if ($response->is_success){
my $data = $response->content;
my $key = $response->header('Key');
my $protocol = $response->header('Protocol') || '?';
if ($response->header('Content-Type') ne 'application/smokeping-config'){
warn "$data\n" unless $data =~ /OK/;
Smokeping::do_debuglog("Sent data to Server. Server said $data");
return undef;
};
if ($protocol ne $PROTOCOL){
warn "WARNING $slave_cfg->{master_url} sent data with protocol $protocol. Expected $PROTOCOL.";
return undef;
}
if (hmac_md5_hex($data,$slave_cfg->{shared_secret}) ne $key){
warn "WARNING $slave_cfg->{master_url} sent data with wrong key";
return undef;
}
# Safe seems to reset SIG on at least FreeBSD, causing slave to crash after first reload
# since all handlers are gone.
my %sig_backup = %SIG;
my $zone = new Safe;
# $zone->permit_only(???); #input welcome as to good settings
my $config = $zone->reval($data);
%SIG = %sig_backup;
if ($@){
warn "WARNING evaluating new config from server failed: $@ --\n$data";
} elsif (defined $config and ref $config eq 'HASH'){
$config->{General}{piddir} = $slave_cfg->{pid_dir};
Smokeping::do_log("Sent data to Server and got new config in response.");
return $config;
}
} else {
# ok did not manage to get our data to the server.
# we store the result so that we can try again later.
warn "WARNING Master said ".$response->status_line()."\n";
nstore $restore, $store;
}
return undef;
}
1;
__END__
=head1 COPYRIGHT
Copyright 2007 by Tobias Oetiker
=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

View File

@@ -0,0 +1,111 @@
#
#
# a few variable definitions to use ciscoRttMonMIB
#
# Joerg Kummer, 10/9/03
#
package Smokeping::ciscoRttMonMIB;
require 5.004;
use vars qw($VERSION);
use Exporter;
use BER;
use SNMP_Session;
use SNMP_util "0.89";
$VERSION = '0.2';
@ISA = qw(Exporter);
sub version () { $VERSION; };
snmpmapOID("rttMonApplVersion", "1.3.6.1.4.1.9.9.42.1.1.1.0");
snmpmapOID("rttMonApplSupportedRttTypesValid", "1.3.6.1.4.1.9.9.42.1.1.7.1.2");
# generic variables for all measurement types
# cisco(9).ciscoMgmt(9).ciscoRttMonMIB(42).ciscoRttMonObjects(1).rttMonCtrl(2).rttMonCtrlAdminTable(1).rttMonCtrlAdminEntry(1)
snmpmapOID("rttMonCtrlAdminIndex", "1.3.6.1.4.1.9.9.42.1.2.1.1.1");
snmpmapOID("rttMonCtrlAdminOwner", "1.3.6.1.4.1.9.9.42.1.2.1.1.2");
snmpmapOID("rttMonCtrlAdminTag", "1.3.6.1.4.1.9.9.42.1.2.1.1.3");
snmpmapOID("rttMonCtrlAdminRttType", "1.3.6.1.4.1.9.9.42.1.2.1.1.4");
snmpmapOID("rttMonCtrlAdminThreshold", "1.3.6.1.4.1.9.9.42.1.2.1.1.5");
snmpmapOID("rttMonCtrlAdminFrequency", "1.3.6.1.4.1.9.9.42.1.2.1.1.6");
snmpmapOID("rttMonCtrlAdminTimeout", "1.3.6.1.4.1.9.9.42.1.2.1.1.7");
snmpmapOID("rttMonCtrlAdminVerifyData", "1.3.6.1.4.1.9.9.42.1.2.1.1.8");
snmpmapOID("rttMonCtrlAdminStatus", "1.3.6.1.4.1.9.9.42.1.2.1.1.9");
snmpmapOID("rttMonCtrlAdminNvgen", "1.3.6.1.4.1.9.9.42.1.2.1.1.10");
#1. For echo, pathEcho and dlsw operations
# cisco(9).ciscoMgmt(9).ciscoRttMonMIB(42).ciscoRttMonObjects(1).rttMonCtrl(2).rttMonEchoAdminTable(2).rttMonEchoAdminEntry (1)
snmpmapOID("rttMonEchoAdminProtocol", "1.3.6.1.4.1.9.9.42.1.2.2.1.1");
snmpmapOID("rttMonEchoAdminTargetAddress", "1.3.6.1.4.1.9.9.42.1.2.2.1.2");
snmpmapOID("rttMonEchoAdminPktDataRequestSize", "1.3.6.1.4.1.9.9.42.1.2.2.1.3");
snmpmapOID("rttMonEchoAdminPktDataResponseSize", "1.3.6.1.4.1.9.9.42.1.2.2.1.4");
snmpmapOID("rttMonEchoAdminTargetPort", "1.3.6.1.4.1.9.9.42.1.2.2.1.5");
snmpmapOID("rttMonEchoAdminSourceAddress", "1.3.6.1.4.1.9.9.42.1.2.2.1.6");
snmpmapOID("rttMonEchoAdminSourcePort", "1.3.6.1.4.1.9.9.42.1.2.2.1.7");
snmpmapOID("rttMonEchoAdminControlEnable", "1.3.6.1.4.1.9.9.42.1.2.2.1.8");
snmpmapOID("rttMonEchoAdminTOS", "1.3.6.1.4.1.9.9.42.1.2.2.1.9");
snmpmapOID("rttMonEchoAdminLSREnable", "1.3.6.1.4.1.9.9.42.1.2.2.1.10");
snmpmapOID("rttMonEchoAdminTargetAddressString", "1.3.6.1.4.1.9.9.42.1.2.2.1.11");
snmpmapOID("rttMonEchoAdminNameServer", "1.3.6.1.4.1.9.9.42.1.2.2.1.12");
snmpmapOID("rttMonEchoAdminOperation", "1.3.6.1.4.1.9.9.42.1.2.2.1.13");
snmpmapOID("rttMonEchoAdminHTTPVersion", "1.3.6.1.4.1.9.9.42.1.2.2.1.14");
snmpmapOID("rttMonEchoAdminURL", "1.3.6.1.4.1.9.9.42.1.2.2.1.15");
snmpmapOID("rttMonEchoAdminCache", "1.3.6.1.4.1.9.9.42.1.2.2.1.16");
snmpmapOID("rttMonEchoAdminInterval", "1.3.6.1.4.1.9.9.42.1.2.2.1.17");
snmpmapOID("rttMonEchoAdminNumPackets", "1.3.6.1.4.1.9.9.42.1.2.2.1.18");
snmpmapOID("rttMonEchoAdminProxy", "1.3.6.1.4.1.9.9.42.1.2.2.1.19");
snmpmapOID("rttMonEchoAdminString1", "1.3.6.1.4.1.9.9.42.1.2.2.1.20");
snmpmapOID("rttMonEchoAdminString2", "1.3.6.1.4.1.9.9.42.1.2.2.1.21");
snmpmapOID("rttMonEchoAdminString3", "1.3.6.1.4.1.9.9.42.1.2.2.1.22");
snmpmapOID("rttMonEchoAdminString4", "1.3.6.1.4.1.9.9.42.1.2.2.1.231");
snmpmapOID("rttMonEchoAdminString5", "1.3.6.1.4.1.9.9.42.1.2.2.1.24");
snmpmapOID("rttMonEchoAdminMode", "1.3.6.1.4.1.9.9.42.1.2.2.1.25");
snmpmapOID("rttMonEchoAdminVrfName", "1.3.6.1.4.1.9.9.42.1.2.2.1.26");
# cisco(9).ciscoMgmt(9).ciscoRttMonMIB(42).ciscoRttMonObjects(1).rttMonCtrl(2).rttMonScheduleAdminTable(5).rttMonScheduleAdminEntry(1)
snmpmapOID("rttMonScheduleAdminRttLife", "1.3.6.1.4.1.9.9.42.1.2.5.1.1");
snmpmapOID("rttMonScheduleAdminRttStartTime", "1.3.6.1.4.1.9.9.42.1.2.5.1.2");
snmpmapOID("rttMonScheduleAdminConceptRowAgeout", "1.3.6.1.4.1.9.9.42.1.2.5.1.3");
# cisco(9).ciscoMgmt(9).ciscoRttMonMIB(42).ciscoRttMonObjects(1).rttMonCtrl(2).rttMonScheduleAdminTable(5).rttMonScheduleAdminEntry(1)
snmpmapOID("rttMonScheduleAdminRttLife", "1.3.6.1.4.1.9.9.42.1.2.5.1.1");
# cisco(9).ciscoMgmt(9).ciscoRttMonMIB(42).ciscoRttMonObjects(1).rttMonCtrl(2).rttMonHistoryAdminTable(8).rttMonHistoryAdminEntry(1)
snmpmapOID("rttMonHistoryAdminNumLives", "1.3.6.1.4.1.9.9.42.1.2.8.1.1");
snmpmapOID("rttMonHistoryAdminNumBuckets", "1.3.6.1.4.1.9.9.42.1.2.8.1.2");
snmpmapOID("rttMonHistoryAdminNumSamples", "1.3.6.1.4.1.9.9.42.1.2.8.1.3");
snmpmapOID("rttMonHistoryAdminFilter", "1.3.6.1.4.1.9.9.42.1.2.8.1.4");
snmpmapOID("rttMonCtrlOperConnectionLostOccurred", "1.3.6.1.4.1.9.9.42.1.2.9.1.5");
snmpmapOID("rttMonCtrlOperTimeoutOccurred", "1.3.6.1.4.1.9.9.42.1.2.9.1.6");
snmpmapOID("rttMonCtrlOperOverThresholdOccurred", "1.3.6.1.4.1.9.9.42.1.2.9.1.7");
snmpmapOID("rttMonCtrlOperNumRtts", "1.3.6.1.4.1.9.9.42.1.2.9.1.8");
snmpmapOID("rttMonCtrlOperRttLife", "1.3.6.1.4.1.9.9.42.1.2.9.1.9");
snmpmapOID("rttMonCtrlOperState", "1.3.6.1.4.1.9.9.42.1.2.9.1.10");
snmpmapOID("rttMonCtrlOperVerifyErrorOccurred", "1.3.6.1.4.1.9.9.42.1.2.9.1.11");
# cisco(9).ciscoMgmt(9).ciscoRttMonMIB(42).ciscoRttMonObjects(1).rttMonHistory(4).rttMonHistoryCollectionTable(1).rttMonHistoryCollectionEntry(1)
snmpmapOID("rttMonStatisticsAdminNumPaths", "1.3.6.1.4.1.9.9.42.1.2.7.1.2");
snmpmapOID("rttMonStatisticsAdminNumHops", "1.3.6.1.4.1.9.9.42.1.2.7.1.3");
# cisco(9).ciscoMgmt(9).ciscoRttMonMIB(42).ciscoRttMonObjects(1).rttMonHistory(4).rttMonHistoryCollectionTable(1).rttMonHistoryCollectionEntry(1)
snmpmapOID("rttMonHistoryCollectionLifeIndex", "1.3.6.1.4.1.9.9.42.1.4.1.1.1");
snmpmapOID("rttMonHistoryCollectionBucketIndex", "1.3.6.1.4.1.9.9.42.1.4.1.1.2");
snmpmapOID("rttMonHistoryCollectionSampleIndex", "1.3.6.1.4.1.9.9.42.1.4.1.1.3");
snmpmapOID("rttMonHistoryCollectionSampleTime", "1.3.6.1.4.1.9.9.42.1.4.1.1.4");
snmpmapOID("rttMonHistoryCollectionAddress", "1.3.6.1.4.1.9.9.42.1.4.1.1.5");
snmpmapOID("rttMonHistoryCollectionCompletionTime", "1.3.6.1.4.1.9.9.42.1.4.1.1.6");
snmpmapOID("rttMonHistoryCollectionSense", "1.3.6.1.4.1.9.9.42.1.4.1.1.7");
snmpmapOID("rttMonHistoryCollectionApplSpecificSense", "1.3.6.1.4.1.9.9.42.1.4.1.1.8");
snmpmapOID("rttMonHistoryCollectionSenseDescription", "1.3.6.1.4.1.9.9.42.1.4.1.1.9");
# return 1 to indicate that all is ok..
1;

View File

@@ -0,0 +1,148 @@
package Smokeping::matchers::Avgratio;
=head1 NAME
Smokeping::matchers::Avgratio - detect changes in average median latency
=head1 OVERVIEW
The Avgratio matcher establishes a historic average median latency over
several measurement rounds. It compares this average, against a second
average latency value again build over several rounds of measurement.
=head1 DESCRIPTION
Call the matcher with the following sequence:
type = matcher
pattern = Avgratio(historic=>a,current=>b,comparator=>o,percentage=>p)
=over
=item historic
The number of median values to use for building the 'historic' average.
=item current
The number of median values to use for building the 'current' average.
=item comparator
Which comparison operator should be used to compare current/historic with percentage.
=item percentage
Right hand side of the comparison.
=back
old <--- historic ---><--- current ---> now
=head1 EXAMPLE
Take build the average median latency over 10 samples, use this to divide the
current average latency built over 2 samples and check if it is bigger than
150%.
Avgratio(historic=>10,current=>2,comparator=>'>',percentage=>150);
avg(current)/avg(historic) > 150/100
This means the matcher will activate when the current latency average is
more than 1.5 times the historic latency average established over the last
10 rounds of measurement.
=head1 COPYRIGHT
Copyright (c) 2004 by OETIKER+PARTNER AG. All rights reserved.
=head1 SPONSORSHIP
The development of this matcher has been sponsored by Virtela Communications, L<http://www.virtela.net/>.
=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 <tobi@oetiker.ch>
=cut
use vars qw($VERSION);
$VERSION = 1.0;
use strict;
use base qw(Smokeping::matchers::base);
use Carp;
sub new(@)
{
my $class = shift;
my $rules = {
historic=>'\d+',
current=>'\d+',
comparator=>'(<|>|<=|>=|==)',
percentage=>'\d+(\.\d+)?' };
my $self = $class->SUPER::new($rules,@_);
$self->{param}{sub} = eval "sub {\$_[0] ".$self->{param}{comparator}." \$_[1]}";
croak "compiling comparator $self->{param}{comparator}: $@" if $@;
$self->{param}{value} = $self->{param}{percentage}/100;
return $self;
}
sub Length($)
{
my $self = shift;
return $self->{param}{historic} + $self->{param}{current};
}
sub Desc ($) {
croak "Detect changes in average median latency";
}
sub avg(@){
my $sum=0;
my $cnt=0;
for (@_){
next unless defined $_;
$sum += $_;
$cnt ++;
}
return $sum/$cnt if $cnt;
return undef;
}
sub Test($$)
{ my $self = shift;
my $data = shift; # @{$data->{rtt}} and @{$data->{loss}}
my $len = $self->Length;
my $rlen = scalar @{$data->{rtt}};
return undef
if $rlen < $len
or (defined $data->{rtt}[-$len] and $data->{rtt}[-$len] eq 'S');
my $ac = $self->{param}{historic};
my $bc = $self->{param}{current};
my $cc = $ac +$bc;
my $ha = avg(@{$data->{rtt}}[-$cc..-$bc-1]);
my $ca = avg(@{$data->{rtt}}[-$bc..-1]);
return undef unless $ha and $ca;
return &{$self->{param}{sub}}($ca/$ha,$self->{param}{value});
}

View File

@@ -0,0 +1,102 @@
package Smokeping::matchers::CheckLatency;
=head1 NAME
Smokeping::matchers::CheckLatency - Edge triggered alert to check latency is under a value for x number of samples
=head1 DESCRIPTION
Call the matcher with the following sequence:
type = matcher
edgetrigger = yes
pattern = CheckLatency(l=>latency to check against,x=>num samples required for a match)
This will create a matcher which checks for "l" latency or greater over "x" samples before raising,
and will hold the alert until "x" samples under "l" before clearing
=head1 COPYRIGHT
Copyright (c) 2006 Dylan C Vanderhoof, Semaphore Corporation
=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
Dylan Vanderhoof <dylanv@semaphore.com>
=cut
use strict;
use base qw(Smokeping::matchers::base);
use vars qw($VERSION);
$VERSION = 1.0;
use Carp;
use List::Util qw(min max);
# I never checked why Median works, but for some reason the first part of the hash was being passed as the rules instead
sub new(@) {
my $class = shift;
my $rules = {
l => '\d+',
x => '\d+'
};
my $self = $class->SUPER::new( $rules, @_ );
return $self;
}
# how many values should we require before raising?
sub Length($) {
my $self = shift;
return $self->{param}{x}; # Because we're edge triggered, we don't need any more than the required samples
}
sub Desc ($) {
croak "Monitor latency with a cooldown period for clearing the alert";
}
sub Test($$) {
my $self = shift;
my $data = shift; # @{$data->{rtt}} and @{$data->{loss}}
my $target = $self->{param}{l} / 1000; # Smokeping reports in seconds
my $count = 0;
my $rtt;
my @rtts = @{ $data->{rtt} };
my $x = min($self->{param}{x}, scalar @{ $data->{rtt} });
#Iterate thru last x number of samples, starting with the most recent
for (my $i=1;$i<=$x;$i++) {
$rtt = $data->{rtt}[$_-$i];
# If there's an S in the array anywhere, return prevmatch
if ( $rtt =~ /S/ ) { return $data->{prevmatch}; }
if ( $data->{prevmatch} ) {
# Alert has already been raised. Evaluate and count consecutive rtt values that are below threshold.
if ( $rtt < $target ) { $count++; }
} else {
# Alert is not raised. Evaluate and count consecutive rtt values that are above threshold.
if ( $rtt >= $target ) { $count++; }
}
}
if ( $count >= $self->{param}{x} ) {
return !$data->{prevmatch};
}
return $data->{prevmatch};
}

View File

@@ -0,0 +1,100 @@
package Smokeping::matchers::CheckLoss;
=head1 NAME
Smokeping::matchers::CheckLoss - Edge triggered alert to check loss is under a value for x number of samples
=head1 DESCRIPTION
Call the matcher with the following sequence:
type = matcher
edgetrigger = yes
pattern = CheckLoss(l=>loss to check against,x=>num samples required for a match)
This will create a matcher which checks for "l" loss or greater over "x" samples before raising,
and will hold the alert until "x" samples under "l" before clearing
=head1 COPYRIGHT
Copyright (c) 2006 Dylan C Vanderhoof, Semaphore Corporation
=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
Dylan Vanderhoof <dylanv@semaphore.com>
=cut
use strict;
use base qw(Smokeping::matchers::base);
use vars qw($VERSION);
$VERSION = 1.0;
use Carp;
use List::Util qw(min max);
# I never checked why Median works, but for some reason the first part of the hash was being passed as the rules instead
sub new(@) {
my $class = shift;
my $rules = {
l => '\d+',
x => '\d+'
};
my $self = $class->SUPER::new( $rules, @_ );
return $self;
}
# how many values should we require before raising?
sub Length($) {
my $self = shift;
return $self->{param}{x}; # Because we're edge triggered, we don't need any more than the required samples
}
sub Desc ($) {
croak "Monitor loss with a cooldown period for clearing the alert";
}
sub Test($$) {
my $self = shift;
my $data = shift; # @{$data->{rtt}} and @{$data->{loss}}
my $target = $self->{param}{l};
my $count = 0;
my $loss;
my $x = min($self->{param}{x}, scalar @{ $data->{loss} });
#Iterate thru last x number of samples, starting with the most recent
for (my $i=1;$i<=$x;$i++) {
$loss = $data->{loss}[$_-$i];
# If there's an S in the array anywhere, return prevmatch
if ( $loss =~ /S/ ) { return $data->{prevmatch}; }
if ( $data->{prevmatch} ) {
# Alert has already been raised. Evaluate and count consecutive loss values that are below threshold.
if ( $loss < $target ) { $count++; }
} else {
# Alert is not raised. Evaluate and count consecutive loss values that are above threshold.
if ( $loss >= $target ) { $count++; }
}
}
if ( $count >= $self->{param}{x} ) {
return !$data->{prevmatch};
}
return $data->{prevmatch};
}

View File

@@ -0,0 +1,158 @@
package Smokeping::matchers::ConsecutiveLoss;
=head1 NAME
Smokeping::matchers::ConsecutiveLoss - Raise/clear alerts according to your choice of threshold and consecutive values
=head1 DESCRIPTION
Use this matcher to raise and clear alerts according to your choice of threshold and consecutive values.
As an example, you can raise an alert on first occurrence of 50% packet loss, but choose to hold the alert
active until packet loss stays below 10% for 5 consecutive measurements.
Add the matcher to your config file using below syntax:
type = matcher
edgetrigger = yes
pattern = ConsecutiveLoss(pctlossraise=>##,stepsraise=>##,pctlossclear=>##,stepsclear=>##)
Replace the ## with integers of your choice, see below for reference:
pctlossraise - Loss values at or above this percentage will raise an alert when...
stepsraise - ... number of consecutive values have been collected
pctlossclear - Loss values below this percentage will clear an alert when...
stepsclear - ... number of consecutive values have been collected
In my environment, I define four alerts for levels like:
+packetloss_significant_instantalert
type = matcher
pattern = ConsecutiveLoss(pctlossraise=>10,stepsraise=>1,pctlossclear=>3,stepsclear=>3)
comment = Instant alert - Significant packet loss detected (At least 10% over 1 cycle). Alert will clear when loss stays at max 2% for 3 cycles
priority = 30
+packetloss_major_instantalert
type = matcher
pattern = ConsecutiveLoss(pctlossraise=>25,stepsraise=>1,pctlossclear=>3,stepsclear=>3)
comment = Instant alert - Major packet loss detected (At least 25% over 1 cycle). Alert will clear when loss stays at max 2% for 3 cycles
priority = 20
+packetloss_significant_consecutivealert
type = matcher
pattern = ConsecutiveLoss(pctlossraise=>10,stepsraise=>3,pctlossclear=>3,stepsclear=>5)
comment = Consecutive occurrence of significant packet loss detected (At least 10% over 3 cycles). Alert will clear when loss stays at max 2% for 5 cycles.
priority = 10
+packetloss_major_consecutivealert
type = matcher
pattern = ConsecutiveLoss(pctlossraise=>25,stepsraise=>3,pctlossclear=>3,stepsclear=>5)
comment = Consecutive occurrence of significant packet loss detected (At least 25% over 3 cycles). Alert will clear when loss stays at max 2% for 5 cycles.
priority = 5
=head1 COPYRIGHT
Copyright (c) 2017 Rickard Borgmaster
=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
Rickard Borgmaster. 2017.
Based on the CheckLoss/Checklatency matchers by Dylan Vanderhoof 2006.
=cut
use strict;
use base qw(Smokeping::matchers::base);
use vars qw($VERSION);
$VERSION = 1.0;
use Carp;
use List::Util qw(min max);
# I never checked why Median works, but for some reason the first part of the hash was being passed as the rules instead
sub new(@) {
my $class = shift;
my $rules = {
pctlossraise => '\d+',
stepsraise => '\d+',
pctlossclear => '\d+',
stepsclear => '\d+'
};
my $self = $class->SUPER::new( $rules, @_ );
return $self;
}
# how many values should we require before raising?
sub Length($) {
my $self = shift;
return max($self->{param}{stepsraise},$self->{param}{stepsclear}); # Minimum number of samples required is the greater of stepsraise/stepsclear
}
sub Desc ($) {
croak "Monitor loss with a cooldown period for clearing the alert";
}
sub Test($$) {
my $self = shift;
my $data = shift; # @{$data->{rtt}} and @{$data->{loss}}
my $count = 0;
my $loss;
my $x;
my $debug = 0; # 0 will suppress debug messages
if ($debug) { print "------------------------------------------------------------------------------------------\n"; }
# Determine number of iterations for the for-loop. if we at all have enough values yet.
if ( $data->{prevmatch} ) {
# Alert state true
if (scalar @{ $data->{loss} } < $self->{param}{stepsclear}) { return $data->{prevmatch}; } # Cannot consider $stepsclear values unless so many values actually exist in array
$x = $self->{param}{stepsclear};
} else {
# Alert state false
if (scalar @{ $data->{loss} } < $self->{param}{stepsraise}) { return $data->{prevmatch}; } # Cannot consider $stepsraise values unless so many values actually exist in array
$x = $self->{param}{stepsraise};
}
if ($debug) { print "Will evaluate $x values because previous alert state= $data->{prevmatch}\n"; }
## Start iterating thru the array
for (my $i=1;$i<=$x;$i++) {
$loss = $data->{loss}[$_-$i];
# If there's an S in the array anywhere, return prevmatch. We do not have enough values yet.
if ( $loss =~ /S/ ) { return $data->{prevmatch}; }
if ( $data->{prevmatch} ) {
# Alert has already been raised. Evaluate and count consecutive loss values that are below threshold.
if ( $loss < $self->{param}{pctlossclear} ) { $count++; }
} else {
# Alert is not raised. Evaluate and count consecutive loss values that are above threshold.
if ( $loss >= $self->{param}{pctlossraise} ) { $count++; }
}
if ($debug) { print "i: $i x: $x count: $count loss: $loss previous alarm state: $data->{prevmatch}\n"; }
}
if ( $count >= $x ) { return !$data->{prevmatch} };
return $data->{prevmatch};
}

View File

@@ -0,0 +1,152 @@
package Smokeping::matchers::ExpLoss;
=head1 NAME
Smokeping::matchers::ExpLoss - exponential weighting matcher for packet loss
with RMON-like thresholds
=head1 DESCRIPTION
Match against exponential weighted average of last samples, thus new values
are more valuable as old ones. Two thresholds - rising and falling - produce
hysteresis loop like in RMON alert subsystem. If the average reaches the
"rising" threshold, matcher go to the "match" state and hold It until the
average drops under the "falling" threshold.
Call the matcher with the following sequence:
type = matcher
pattern = ExpLoss(hist => <hist>, rising=><rising> \
[,falling => <falling>] [,skip=><stat>] [,fast=><fast>])
Arguments:
hist - number of samples to weight against; weight will be disposed with
exponential decreasing manner from newest to oldest, so that the
oldest sample would have 1% significance;
rising - rising threshold for packet loss, 0-100%
falling - falling threshold for packet loss, default is <rising>
skip - skip <skip> number of samples after startup before "fire" alerts.
fast - use <fast> samples for fast transition: if the values of last <fast>
samples more then <rising> - take "match" state, if less then
<falling> - take "no match" state.
Note:
If the actual history is less then <hist> value then this value is taken
as the actual history.
=head1 COPYRIGHT
Copyright (c) 2008 Veniamin Konoplev
Developed in cooperation with EU EGEE project
=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
Veniamin Konoplev E<lt>vkonoplev@acm.orgE<gt>
=cut
use strict;
use base qw(Smokeping::matchers::base);
use vars qw($VERSION);
$VERSION = 1.0;
use Carp;
sub new(@) {
my $class = shift;
my $rules = {
hist => '\d+',
rising => '\d+(\.\d+)?',
falling => '\d+(\.\d+)?',
skip => '\d+',
fast => '\d+',
};
my $self = $class->SUPER::new( $rules, @_ );
return $self;
}
# how many values should we require before raising?
sub Length($) {
my $self = shift;
return $self->{param}{hist}; #
}
sub Desc ($) {
croak "Monitor if exponential weighted loss is in interval";
}
sub Test($$) {
my $self = shift;
my $data = shift; # @{$data->{rtt}} and @{$data->{loss}}
my $hist = $self->{param}{hist}; # history length
my $skip = ($self->{param}{skip} || 0); # skip <skip> samples before start
my $fast = ($self->{param}{fast} || 0); # use last <fast> samples for fast alerts
return undef if scalar(@{ $data->{loss}}) <= $skip+1;
# calculate alpha factor to obtain 1% significance
# of the old probes at the <hist> boundary
my $alfa = 1-0.01**(1/$hist);
my $rising = $self->{param}{rising};
my $falling = $self->{param}{falling} // $rising;
my $result = 0; # initialize the filter as zero;
my $loss;
my $sum = 0;
my $num = 0;
my $rising_cnt = 0;
my $falling_cnt = 0;
foreach $loss ( @{ $data->{loss} } ) {
# If there's an S in the array anywhere, return prevmatch
next if ( $loss =~ /S/ or $loss =~ /U/);
# update the filter
$result = (1-$alfa)*$result+$alfa*$loss;
$sum += $loss;
$num++;
if ($fast) {
$rising_cnt = ($loss >= $rising) ? $rising_cnt + 1 : 0;
$falling_cnt = ($loss <= $falling) ? $falling_cnt + 1 : 0;
}
}
return undef if $num == 0;
#
if ($fast) {
return 1 if $rising_cnt >= $fast;
return "" if $falling_cnt >= $fast;
}
# correct filter result as if it was initialized with "average"
$result += ($sum/$num)*((1-$alfa)**$num);
my $res = (($result >= $rising) or ($data->{prevmatch} and $result >= $falling));
# some debug stuff
if (0) {
my $d = `date`;
chomp $d;
my $array = join ":", @{ $data->{loss}};
`echo $d $data->{target} $array $result. >> /tmp/matcher.log` if $rising == 0;
}
return $res;
}
1;

View File

@@ -0,0 +1,101 @@
package Smokeping::matchers::Median;
=head1 NAME
Smokeping::matchers::Median - Find persistent changes in latency
=head1 OVERVIEW
The idea behind this matcher is to find sustained changes in latency.
The median matcher takes a number of past median latencies. It splits the latencies into
two groups (old and new) and again finds the median for each groups. If the
difference between the two medians is bigger than a certain value, it will
give a match.
=head1 DESCRIPTION
Call the matcher with the following sequence:
type = matcher
pattern = Median(old=>x,new=>y,diff=>z)
This will create a matcher which consumes x+y latency-datapoints, builds the
two medians and the matches if the difference between the median latency is
larger than z seconds.
=head1 COPYRIGHT
Copyright (c) 2004 by OETIKER+PARTNER AG. All rights 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 <tobi@oetiker.ch>
=cut
use strict;
use base qw(Smokeping::matchers::base);
use vars qw($VERSION);
$VERSION = 1.0;
use Carp;
sub new(@)
{
my $class = shift;
my $rules = {
old=>'\d+',
new=>'\d+',
diff=>'\d+(\.\d+)?' };
my $self = $class->SUPER::new($rules,@_);
return $self;
}
# how many values does the matcher need to do it's magic
sub Length($)
{
my $self = shift;
return $self->{param}{old} + $self->{param}{new};
}
sub Desc ($) {
croak "Find changes in median latency";
}
sub Test($$)
{ my $self = shift;
my $data = shift; # @{$data->{rtt}} and @{$data->{loss}}
my $ac = $self->{param}{old};
my $bc = $self->{param}{new};
my $cc = $ac +$bc;
my $count = scalar @{$data->{rtt}};
$cc = $count if $count < $cc;
$bc = $count if $count < $bc;
my $oldm = robust_median(@{$data->{rtt}}[-$cc..-$bc-1]);
my $newm = robust_median(@{$data->{rtt}}[-$bc..-1]);
return abs($oldm-$newm) > $self->{param}{diff};
}
sub robust_median(@){
my @numbers = sort {$a <=> $b} grep { defined $_ and $_ =~ /\d/ } @_;
my $count = $#numbers;
return 0 if $count < 0;
return ($count / 2 == int($count/2)) ? $numbers[$count/2] : ($numbers[$count/2+0.5] + $numbers[$count/2-0.5])/2;
}

View File

@@ -0,0 +1,118 @@
package Smokeping::matchers::Medratio;
=head1 NAME
Smokeping::matchers::Medratio - detect changes in the latency median
=head1 OVERVIEW
The Medratio matcher establishes a historic median latency over
several measurement rounds. It compares this median, against a second
median latency value again build over several rounds of measurement.
By looking at the median value this matcher is largely immune against spikes
and will only react to long term developments.
=head1 DESCRIPTION
Call the matcher with the following sequence:
type = matcher
pattern = Medratio(historic=>a,current=>b,comparator=>o,percentage=>p)
=over
=item historic
The number of values to use for building the 'historic' median.
=item current
The number of values to use for building the 'current' median.
=item comparator
Which comparison operator should be used to compare current/historic with percentage.
=item percentage
Right hand side of the comparison.
=back
old <--- historic ---><--- current ---> now
=head1 EXAMPLE
Take the 12 last median values. Build the median out of the first 10
and the median from the other 2 values. Divide the results and decide
if it is bigger than 150 percent.
Medratio(historic=>10,current=>2,comparator=>'>',percentage=>150);
med(current)/med(historic) > 150/100
This means the matcher will activate when the current latency median is
more than 1.5 times the historic latency median established over the last
10 rounds of measurement.
=head1 COPYRIGHT
Copyright (c) 2006 by OETIKER+PARTNER AG. All rights reserved.
=head1 SPONSORSHIP
The development of this matcher has been paid for by Virtela
Communications, L<http://www.virtela.net/>.
=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 <tobi@oetiker.ch>
=cut
use vars qw($VERSION);
$VERSION = 1.0;
use strict;
use base qw(Smokeping::matchers::Avgratio);
use Carp;
sub Desc ($) {
croak "Detect changes in median latency";
}
sub Test($$)
{ my $self = shift;
my $data = shift; # @{$data->{rtt}} and @{$data->{loss}}
my $len = $self->Length;
my $rlen = scalar @{$data->{rtt}};
return undef
if $rlen < $len
or (defined $data->{rtt}[-$len] and $data->{rtt}[-$len] eq 'S');
my $ac = $self->{param}{historic};
my $bc = $self->{param}{current};
my $cc = $ac +$bc;
my $hm = (sort {$a <=> $b} @{$data->{rtt}}[-$cc..-$bc-1])[int($ac/2)];
my $cm = (sort {$a <=> $b} @{$data->{rtt}}[-$bc..-1])[int($bc/2)];
return undef unless $hm and $cm;
return &{$self->{param}{sub}}($cm/$hm,$self->{param}{value});
}

View File

@@ -0,0 +1,135 @@
package Smokeping::matchers::base;
=head1 NAME
Smokeping::matchers::base - Base Class for implementing SmokePing Matchers
=head1 OVERVIEW
This is the base class for writing SmokePing matchers. Every matcher must
inherit from the base class and provide it's own methods for the 'business'
logic.
Note that the actual matchers must have at least one capital letter in their
name, to differentiate them from the base class(es).
=head1 DESCRIPTION
Every matcher must provide the following methods:
=cut
use vars qw($VERSION);
use Carp;
$VERSION = 1.0;
use strict;
=head2 new
The new method expects hash elements as an argument
eg new({x=>'\d+',y=>'\d+'},x=>1,y=>2). The first part is
a syntax rule for the arguments it should expect and the second part
are the arguments itself. The first part will be supplied
by the child class as it calls the parent method.
=cut
sub new(@)
{
my $this = shift;
my $class = ref($this) || $this;
my $rules = shift;
my $self = { param => { @_ } };
foreach my $key (keys %{$self->{param}}){
my $regex = $rules->{$key};
croak "key '$key' is not known by this matcher" unless defined $rules->{$key};
croak "key '$key' contains invalid data: '$self->{param}{$key}'" unless $self->{param}{$key} =~ m/^$regex$/;
}
bless $self, $class;
return $self;
}
=head2 Length
The Length method returns the number of values the
matcher will expect from SmokePing. This method must
be overridden by the children of the base class.
=cut
sub Length($)
{
my $self = shift;
croak "SequenceLength must be overridden by the subclass";
}
=head2 Desc
Simply return the description of the function. This method must
be overwritten by a children of the base class.
=cut
sub Desc ($) {
croak "MatcherDesc must be overridden by the subclass";
}
=head2 Test
Run the matcher and return true or false. The Test method is called
with a hash containing two arrays giving it access to both rtt and loss values.
my $data=shift;
my @rtt = @{$data->{rtt}};
my @loss = @{$data->{loss}};
The arrays are ordered from old to new.
@rdd[old..new]
There may be more than the expected number of elements in this array. Address them with
$x[-1] to $x[-max].
There's also a key called 'prevmatch' in the hash. It contains the
value returned by the previous call of the 'Test' method. This allows
for somewhat more intelligent alerting due to state awareness.
my $prevmatch = $data->{prevmatch};
=cut
sub Test($$)
{ my $self = shift;
my $data = shift; # @{$data->{rtt}} and @{$data->{loss}}
croak "Match must be overridden by the subclass";
}
=head1 COPYRIGHT
Copyright (c) 2004 by OETIKER+PARTNER AG. All rights 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 <tobi@oetiker.ch>
=cut

View File

@@ -0,0 +1,80 @@
#
#
# a few variable definitions to use pingMIB
#
# Bill Fenner, 10/23/06
# Based on ciscoRttMonMIB.pm
#
package Smokeping::pingMIB;
require 5.004;
use vars qw($VERSION);
use Exporter;
use BER;
use SNMP_Session;
use SNMP_util "0.89";
$VERSION = '0.1';
@ISA = qw(Exporter);
sub version () { $VERSION; };
# Scalars:
snmpmapOID("pingMaxConcurrentRequests", "1.3.6.1.2.1.80.1.1.0");
# pingCtlTable
snmpmapOID("pingCtlOwnerIndex", "1.3.6.1.2.1.80.1.2.1.1");
snmpmapOID("pingCtlTestName", "1.3.6.1.2.1.80.1.2.1.2");
snmpmapOID("pingCtlTargetAddressType", "1.3.6.1.2.1.80.1.2.1.3");
snmpmapOID("pingCtlTargetAddress", "1.3.6.1.2.1.80.1.2.1.4");
snmpmapOID("pingCtlDataSize", "1.3.6.1.2.1.80.1.2.1.5");
snmpmapOID("pingCtlTimeOut", "1.3.6.1.2.1.80.1.2.1.6");
snmpmapOID("pingCtlProbeCount", "1.3.6.1.2.1.80.1.2.1.7");
snmpmapOID("pingCtlAdminStatus", "1.3.6.1.2.1.80.1.2.1.8");
snmpmapOID("pingCtlDataFill", "1.3.6.1.2.1.80.1.2.1.9");
snmpmapOID("pingCtlFrequency", "1.3.6.1.2.1.80.1.2.1.10");
snmpmapOID("pingCtlMaxRows", "1.3.6.1.2.1.80.1.2.1.11");
snmpmapOID("pingCtlStorageType", "1.3.6.1.2.1.80.1.2.1.12");
snmpmapOID("pingCtlTrapGeneration", "1.3.6.1.2.1.80.1.2.1.13");
snmpmapOID("pingCtlTrapProbeFailureFilter", "1.3.6.1.2.1.80.1.2.1.14");
snmpmapOID("pingCtlTrapTestFailureFilter", "1.3.6.1.2.1.80.1.2.1.15");
snmpmapOID("pingCtlType", "1.3.6.1.2.1.80.1.2.1.16");
snmpmapOID("pingCtlDescr", "1.3.6.1.2.1.80.1.2.1.17");
snmpmapOID("pingCtlSourceAddressType", "1.3.6.1.2.1.80.1.2.1.18");
snmpmapOID("pingCtlSourceAddress", "1.3.6.1.2.1.80.1.2.1.19");
snmpmapOID("pingCtlIfIndex", "1.3.6.1.2.1.80.1.2.1.20");
snmpmapOID("pingCtlByPassRouteTable", "1.3.6.1.2.1.80.1.2.1.21");
snmpmapOID("pingCtlDSField", "1.3.6.1.2.1.80.1.2.1.22");
snmpmapOID("pingCtlRowStatus", "1.3.6.1.2.1.80.1.2.1.23");
# pingResultsTable
snmpmapOID("pingResultsOperStatus", "1.3.6.1.2.1.80.1.3.1.1");
snmpmapOID("pingResultsIpTargetAddressType", "1.3.6.1.2.1.80.1.3.1.2");
snmpmapOID("pingResultsIpTargetAddress", "1.3.6.1.2.1.80.1.3.1.3");
snmpmapOID("pingResultsMinRtt", "1.3.6.1.2.1.80.1.3.1.4");
snmpmapOID("pingResultsMaxRtt", "1.3.6.1.2.1.80.1.3.1.5");
snmpmapOID("pingResultsAverageRtt", "1.3.6.1.2.1.80.1.3.1.6");
snmpmapOID("pingResultsProbeResponses", "1.3.6.1.2.1.80.1.3.1.7");
snmpmapOID("pingResultsSentProbes", "1.3.6.1.2.1.80.1.3.1.8");
snmpmapOID("pingResultsRttSumOfSquares", "1.3.6.1.2.1.80.1.3.1.9");
snmpmapOID("pingResultsLastGoodProbe", "1.3.6.1.2.1.80.1.3.1.10");
# pingProbeHistoryTable
snmpmapOID("pingProbeHistoryIndex", "1.3.6.1.2.1.80.1.4.1.1");
snmpmapOID("pingProbeHistoryResponse", "1.3.6.1.2.1.80.1.4.1.2");
snmpmapOID("pingProbeHistoryStatus", "1.3.6.1.2.1.80.1.4.1.3");
snmpmapOID("pingProbeHistoryLastRC", "1.3.6.1.2.1.80.1.4.1.4");
snmpmapOID("pingProbeHistoryTime", "1.3.6.1.2.1.80.1.4.1.5");
# pingImplementationTypeDomains - if we end up supporting other ping types
snmpmapOID("pingIcmpEcho", "1.3.6.1.2.1.80.3.1");
snmpmapOID("pingUdpEcho", "1.3.6.1.2.1.80.3.2");
snmpmapOID("pingSnmpQuery", "1.3.6.1.2.1.80.3.3");
snmpmapOID("pingTcpConnectionAttempt", "1.3.6.1.2.1.80.3.4");
# return 1 to indicate that all is ok..
1;

View File

@@ -0,0 +1,357 @@
package Smokeping::probes::AnotherCurl;
=head1 301 Moved Permanently
This is a Smokeping probe module. Please use the command
C<smokeping -man Smokeping::probes::AnotherCurl>
to view the documentation or the command
C<smokeping -makepod Smokeping::probes::AnotherCurl>
to generate the POD document.
=cut
use strict;
use base qw(Smokeping::probes::basefork);
use Carp;
my $DEFAULTBIN = "/usr/bin/curl";
sub pod_hash {
return {
name => "Smokeping::probes::AnotherCurl - a curl(1) probe for SmokePing",
overview => "Fetches an HTTP or HTTPS URL using curl(1).",
description => "(see curl(1) for details of the options below)",
authors => <<'DOC',
Gerald Combs <gerald [AT] ethereal.com>
Niko Tyni <ntyni@iki.fi>
Jean Baptiste Favre <smokeping@jbfavre.org>
DOC
notes => <<DOC,
You should consider setting a lower value for the C<pings> variable than the
default 20, as repetitive URL fetching may be quite heavy on the server.
The URL to be tested used to be specified by the variable 'url' in earlier
versions of Smokeping, and the 'host' setting did not influence it in any
way. The variable name has now been changed to 'urlformat', and it can
(and in most cases should) contain a placeholder for the 'host' variable.
Legacy Curl probe only returns page load time. With AnotherCurl, you can
specify which 'write-out' value you want to get (please refer to curl(1) for
more details about write_out option.
DOC
see_also => "curl(1), L<http://curl.haxx.se/>",
}
}
sub probevars {
my $class = shift;
my $h = $class->SUPER::probevars;
delete $h->{timeout};
return $class->_makevars($h, {
binary => {
_doc => "The location of your curl binary.",
_default => $DEFAULTBIN,
_sub => sub {
my $val = shift;
return "ERROR: Curl 'binary' $val does not point to an executable"
unless -f $val and -x _;
return undef;
},
},
});
}
sub targetvars {
my $class = shift;
return $class->_makevars($class->SUPER::targetvars, {
_mandatory => [ 'urlformat' ],
agent => {
_doc => <<DOC,
The "-A" curl(1) option. This is a full HTTP User-Agent header including
the words "User-Agent:". Note that it does not need any quotes around it.
DOC
_example => 'User-Agent: Lynx/2.8.4rel.1 libwww-FM/2.14 SSL-MM/1.4.1 OpenSSL/0.9.6c',
_sub => sub {
my $val = shift;
return "The Curl 'agent' string does not need any quotes around it anymore."
if $val =~ /^["']/ or $val =~ /["']$/;
return undef;
},
},
timeout => {
_doc => qq{The "-m" curl(1) option. Maximum timeout in seconds.},
_re => '\d+',
_example => 20,
_default => 10,
},
interface => {
_doc => <<DOC,
The "--interface" curl(1) option. Bind to a specific interface, IP address or
host name.
DOC
_example => 'eth0',
},
ssl2 => {
_doc => qq{The "-2" curl(1) option. Force SSL2.},
_example => 1,
},
urlformat => {
_doc => <<DOC,
The template of the URL to fetch. Can be any one that curl supports.
Any occurrence of the string '%host%' will be replaced with the
host to be probed.
DOC
_example => "http://%host%/",
},
insecure_ssl => {
_doc => <<DOC,
The "-k" curl(1) option. Accept SSL connections that don't have a secure
certificate chain to a trusted CA. Note that if you are going to monitor
https targets, you'll probably have to either enable this option or specify
the CA path to curl through extraargs below. For more info, see the
curl(1) manual page.
DOC
_example => 1,
},
extrare=> {
_doc => <<DOC,
The regexp used to split the extraargs string into an argument list,
in the "/regexp/" notation. This contains just the space character
(" ") by default, but if you need to specify any arguments containing spaces,
you can set this variable to a different value.
DOC
_default => "/ /",
_example => "/ /",
_sub => sub {
my $val = shift;
return "extrare should be specified in the /regexp/ notation"
unless $val =~ m,^/.*/$,;
return undef;
},
},
follow_redirects => {
_doc => <<DOC,
If this variable is set to 'yes', curl will follow any HTTP redirection steps (the '-L' option).
If set to 'no', HTTP Location: headers will not be followed. See also 'include_redirects'.
DOC
_default => "no",
_re => "(yes|no)",
_example => "yes",
},
include_redirects => {
_doc => <<DOC,
If this variable is set to 'yes', the measurement result will include the time
spent on following any HTTP redirection steps. If set to 'no', only the last
step is measured. See also 'follow_redirects'.
DOC
_default => "no",
_re => "(yes|no)",
_example => "yes",
},
extraargs => {
_doc => <<DOC,
Any extra arguments you might want to hand to curl(1). The arguments
should be separated by the regexp specified in "extrare", which
contains just the space character (" ") by default.
Note that curl will be called with the resulting list of arguments
without any shell expansion. If you need to specify any arguments
containing spaces, you should set "extrare" to something else.
As a complicated example, to explicitly set the "Host:" header in Curl
requests, you need to set "extrare" to something else, eg. "/;/",
and then specify C<extraargs = --header;Host: www.example.com>.
DOC
_example => "-6 --head --user user:password",
},
write_out => {
_doc => <<DOC,
Choose which write-out value you want to send to Smokeping. Value can
be one of: 'time_appconnect', 'time_connect', 'time_namelookup',
'time_pretransfer', 'time_redirect', 'time_starttransfer', 'time_total'.
Default behaviour is the same as legacy Curl probe one.
DOC
},
});
}
# derived class will mess with this through the 'features' method below
my $featurehash = {
agent => "-A",
timeout => "-m",
interface => "--interface",
};
sub features {
my $self = shift;
my $newval = shift;
$featurehash = $newval if defined $newval;
return $featurehash;
}
sub new {
my $proto = shift;
my $class = ref($proto) || $proto;
my $self = $class->SUPER::new(@_);
$self->_init if $self->can('_init');
# no need for this if running as a CGI
$self->test_usage unless $ENV{SERVER_SOFTWARE};
return $self;
}
# warn about unsupported features
sub test_usage {
my $self = shift;
my $bin = $self->{properties}{binary};
my @unsupported;
my $arghashref = $self->features;
my %arghash = %$arghashref;
my $curl_man = `$bin --help`;
for my $feature (keys %arghash) {
next if $curl_man =~ /\Q$arghash{$feature}/;
push @unsupported, $feature;
$self->do_log("Note: your curl doesn't support the $feature feature (option $arghash{$feature}), disabling it");
}
map { delete $arghashref->{$_} } @unsupported;
# if ($curl_man !~ /\stime_redirect\s/) {
# $self->do_log("Note: your curl doesn't support the 'time_redirect' output variable; 'include_redirects' will not function.");
# }
return;
}
sub ProbeDesc($) {
return "URLs using curl(1)";
}
# other than host, count and protocol-specific args come from here
sub make_args {
my $self = shift;
my $target = shift;
my @args;
my %arghash = %{$self->features};
for (keys %arghash) {
my $val = $target->{vars}{$_};
push @args, ($arghash{$_}, $val) if defined $val;
}
return @args;
}
# This is what derived classes will override
sub proto_args {
my $self = shift;
my $target = shift;
# XXX - It would be neat if curl had a "time_transfer". For now,
# we take the total time minus the DNS lookup time.
my @args = ("-w", "Total: %{time_total} DNS: %{time_namelookup} Redirect: %{time_redirect} Connect: %{time_connect} Appconnect: %{time_appconnect} Pretransfert: %{time_pretransfer} Starttransfert: %{time_starttransfer}\\n");
my $ssl2 = $target->{vars}{ssl2};
push (@args, "-2") if $ssl2;
my $insecure_ssl = $target->{vars}{insecure_ssl};
push (@args, '-k') if $insecure_ssl;
my $follow = $target->{vars}{follow_redirects};
push (@args, '-L') if $follow eq "yes";
return(@args);
}
sub extra_args {
my $self = shift;
my $target = shift;
my $args = $target->{vars}{extraargs};
return () unless defined $args;
my $re = $target->{vars}{extrare};
($re =~ m,^/(.*)/$,) and $re = qr{$1};
return split($re, $args);
}
sub make_commandline {
my $self = shift;
my $target = shift;
my $count = shift;
my @args = $self->make_args($target);
my $url = $target->{vars}{urlformat};
my $host = $target->{addr};
$url =~ s/%host%/$host/g;
my @urls = split(/\s+/, $url);
push @args, ("-o", "/dev/null") for (@urls);
push @args, $self->proto_args($target);
push @args, $self->extra_args($target);
return ($self->{properties}{binary}, @args, @urls);
}
sub pingone {
my $self = shift;
my $t = shift;
my @cmd = $self->make_commandline($t);
$self->do_debug("executing command list " . join(",", map { qq('$_') } @cmd));
my @times;
my $count = $self->pings($t);
for (my $i = 0 ; $i < $count; $i++) {
open(P, "-|") or exec @cmd;
my $val;
while (<P>) {
chomp;
/^Total: (\d+\.\d+) DNS: (\d+\.\d+) Redirect: (\d+\.\d+) Connect: (\d+\.\d+) Appconnect: (\d+\.\d+) Pretransfert: (\d+\.\d+) Starttransfert: (\d+\.\d+)?/ and do {
# Total: time_total
# DNS: time_namelookup
# Redirect: time_redirect
# Connect: time_connect
# Appconnect: time_appconnect
# Pretransfert: time_pretransfer
# Starttransfert: time_starttransfer
# Default is current behaviour where we take total minus DNS resolution.
if ($t->{vars}{write_out} eq 'time_total') {$val += $1}
elsif ($t->{vars}{write_out} eq 'time_namelookup') {$val += $2}
elsif ($t->{vars}{write_out} eq 'time_redirect') {$val += $3}
elsif ($t->{vars}{write_out} eq 'time_connect') {$val += $4}
elsif ($t->{vars}{write_out} eq 'time_appconnect') {$val += $5}
elsif ($t->{vars}{write_out} eq 'time_pretransfer') {$val += $6}
elsif ($t->{vars}{write_out} eq 'time_starttransfer') {$val += $7}
else {$val += $1 - $2;}
if ($t->{vars}{include_redirects} eq "yes" and defined $3) {
$val += $3;
}
$self->do_debug("curl output: '$_', result: $val");
};
}
close P;
if ($?) {
my $status = $? >> 8;
my $signal = $? & 127;
my $why = "with status $status";
$why .= " [signal $signal]" if $signal;
# only log warnings on the first ping of the first ping round
my $function = ($self->rounds_count == 1 and $i == 0) ?
"do_log" : "do_debug";
$self->$function(qq(WARNING: curl exited $why on $t->{addr}));
}
push @times, $val if defined $val;
}
# carp("Got @times") if $self->debug;
return sort { $a <=> $b } @times;
}
1;

View File

@@ -0,0 +1,247 @@
package Smokeping::probes::AnotherDNS;
=head1 301 Moved Permanently
This is a Smokeping probe module. Please use the command
C<smokeping -man Smokeping::probes::AnotherDNS>
to view the documentation or the command
C<smokeping -makepod Smokeping::probes::AnotherDNS>
to generate the POD document.
=cut
use strict;
use base qw(Smokeping::probes::basefork);
use IPC::Open3;
use Symbol;
use Carp;
use Time::HiRes qw(sleep ualarm gettimeofday tv_interval);
use IO::Socket;
use IO::Select;
use Net::DNS;
sub pod_hash {
return {
name => <<DOC,
Smokeping::probes::AnotherDNS - Alternate DNS Probe
DOC
description => <<DOC,
Like DNS, but uses Net::DNS and Time::HiRes instead of dig. This probe does
*not* retry the request three times before it is considered "lost", like dig and
other resolver do by default. If operating as caching Nameserver, BIND (and
maybe others) expect clients to retry the request if the answer is not in the
cache. So, ask the nameserver for something that he is authoritative for if you
want measure the network packet loss correctly.
If you have a really fast network and nameserver, you will notice that this
probe reports the query time in microsecond resolution. :-)
DOC
authors => <<'DOC',
Christoph Heine <Christoph.Heine@HaDiKo.DE>
DOC
}
}
sub new($$$) {
my $proto = shift;
my $class = ref($proto) || $proto;
my $self = $class->SUPER::new(@_);
return $self;
}
sub ProbeDesc($) {
my $self = shift;
return "DNS requests";
}
sub pingone ($) {
my $self = shift;
my $target = shift;
my $host = $target->{addr};
my $lookuphost = $target->{vars}{lookup};
my $mininterval = $target->{vars}{mininterval};
my $recordtype = $target->{vars}{recordtype};
my $authoritative = $target->{vars}{authoritative};
my $timeout = $target->{vars}{timeout};
my $port = $target->{vars}{port};
my $ipversion = $target->{vars}{ipversion};
my $protocol = $target->{vars}{protocol};
my $require_noerror = $target->{vars}{require_noerror};
my $require_nxdomain = $target->{vars}{require_nxdomain};
my $expect_text = $target->{vars}{expect_text};
$lookuphost = $target->{addr} unless defined $lookuphost;
if ($require_nxdomain eq 1 && $require_noerror eq 1) {
$self->do_log("ERROR: require_nxdomain and require_noerror can't both be enabled for the same target");
return;
}
my $sock = 0;
if ($ipversion == 6) {
require IO::Socket::INET6;
$sock = IO::Socket::INET6->new(
"PeerAddr" => $host,
"PeerPort" => $port,
"Proto" => $protocol,
);
} else {
require IO::Socket::INET;
$sock = IO::Socket::INET->new(
"PeerAddr" => $host,
"PeerPort" => $port,
"Proto" => $protocol,
);
}
my $sel = IO::Select->new($sock);
my @times;
my $elapsed;
for ( my $run = 0 ; $run < $self->pings($target) ; $run++ ) {
my $expectMatched = 0;
if (defined $elapsed) {
my $timeleft = $mininterval - $elapsed;
sleep $timeleft if $timeleft > 0;
}
my $query = Net::DNS::Packet->new( $lookuphost, $recordtype );
$query->header->rd(!$authoritative);
my $packet = $query->data;
my $t0 = [gettimeofday()];
$sock->send($packet);
my ($ready) = $sel->can_read($timeout);
my $t1 = [gettimeofday()];
$elapsed = tv_interval( $t0, $t1 );
if ( defined $ready ) {
my $buf = '';
$ready->recv( $buf, 512 );
my ($recvPacket, $err) = Net::DNS::Packet->new(\$buf);
if (defined $recvPacket) {
my $recvHeader = $recvPacket->header();
next if $require_nxdomain && $recvHeader->rcode ne "NXDOMAIN";
if ($expect_text ne "" && $recvHeader->ancount > 0) {
#Test the answer RR(s) for the expected response string
foreach ($recvPacket->answer()) {
#$self->do_debug("Checking for $expect_text in " . $_->string);
if (index($_->string, $expect_text) != -1) {
$expectMatched = 1;
last;
}
}
}
next if $expect_text ne "" && $expectMatched eq 0;
next if $recvHeader->id != $query->header->id;
next if $authoritative && !$recvHeader->aa;
next if $recvHeader->ancount() < $target->{vars}{require_answers};
if (not $require_noerror) {
push @times, $elapsed;
} else {
# Check the Response Code for the NOERROR.
if ($recvHeader->rcode() eq "NOERROR") {
push @times, $elapsed;
}
}
}
}
}
@times =
map { sprintf "%.10e", $_ } sort { $a <=> $b } grep { $_ ne "-" } @times;
return @times;
}
sub probevars {
my $class = shift;
my $h = $class->SUPER::probevars;
delete $h->{timeout};
return $h;
}
sub targetvars {
my $class = shift;
return $class->_makevars($class->SUPER::targetvars, {
lookup => {
_doc => <<DOC,
Name of the host to look up in the dns.
DOC
_example => 'www.example.org',
},
mininterval => {
_doc => <<DOC,
Minimum time between sending two lookup queries in (possibly fractional) seconds.
DOC
_default => .5,
_re => '(\d*\.)?\d+',
},
authoritative => {
_doc => 'Send non-recursive queries and require authoritative answers.',
_default => 0,
},
require_noerror => {
_doc => 'Only Count Answers with Response Status NOERROR.',
_default => 0,
},
require_answers => {
_doc => 'Only Count Answers with answer count >= this value.',
_default => 0,
},
recordtype => {
_doc => 'Record type to look up.',
_default => 'A',
},
timeout => {
_doc => 'Timeout for a single request in seconds.',
_default => 5,
_re => '\d+',
},
expect_text => {
_doc => <<DOC,
A string that should be present in the DNS answer. This can be used
to verify that an A record contains the expected IP address, a PTR
record reflects the expected hostname, etc. If the query returns
multiple records, any single match will pass the test.
DOC
_example => '192.168.50.60',
},
require_nxdomain => {
_doc => <<DOC,
Set to 1 if NXDOMAIN should be interpreted as success instead of
failure. This reverses the normal behavior of the probe. Example uses
include testing a DNS firewall, verifying that a mail server IP is
not listed on a DNSBL, or other scenarios where NXDOMAIN is desired.
DOC
_default => 0,
_example => 0,
_re => '[01]',
},
port => {
_doc => 'The UDP Port to use.',
_default => 53,
_re => '\d+',
},
protocol => {
_doc => 'The Network Protocol to use.',
_default => 'udp',
_re => '(udp|UDP|tcp|TCP)',
},
ipversion => {
_doc => <<DOC,
The IP protocol used. Possible values are "4" and "6".
Passed to echoping(1) as the "-4" or "-6" options.
DOC
_example => 4,
_default => 4,
_re => '[46]',
},
});
}
1;

View File

@@ -0,0 +1,238 @@
package Smokeping::probes::AnotherSSH;
=head1 301 Moved Permanently
This is a Smokeping probe module. Please use the command
C<smokeping -man Smokeping::probes::AnotherSSH>
to view the documentation or the command
C<smokeping -makepod Smokeping::probes::AnotherSSH>
to generate the POD document.
=cut
use strict;
use base qw(Smokeping::probes::basefork);
use Carp;
use Time::HiRes qw(sleep ualarm gettimeofday tv_interval);
use IO::Select;
use Socket;
use Fcntl;
sub pod_hash {
return {
name => <<DOC,
Smokeping::probes::AnotherSSH - Another SSH probe
DOC
description => <<DOC,
Latency measurement using SSH. This generates Logfile messages on the other
Host, so get permission from the owner first!
DOC
authors => <<'DOC',
Christoph Heine <Christoph.Heine@HaDiKo.DE>
DOC
}
}
sub new($$$) {
my $proto = shift;
my $class = ref($proto) || $proto;
my $self = $class->SUPER::new(@_);
return $self;
}
sub ProbeDesc($) {
my $self = shift;
return "SSH connections";
}
sub pingone ($) {
my $self = shift;
my $target = shift;
my $host = $target->{addr};
# Time
my $mininterval = $target->{vars}{mininterval};
# Our greeting string.
my $greeting = $target->{vars}{greeting};
# Interval to measure
my $interval = $target->{vars}{interval};
# Connect to this port.
my $port = $target->{vars}{port};
#Timeout for the select() calls.
my $timeout = $target->{vars}{timeout};
my @times; # Result times
my $t0;
for ( my $run = 0 ; $run < $self->pings($target) ; $run++ ) {
if (defined $t0) {
my $elapsed = tv_interval($t0, [gettimeofday()]);
my $timeleft = $mininterval - $elapsed;
sleep $timeleft if $timeleft > 0;
}
my ($t1,$t2,$t3); # Timestamps.
#Temporary variables to play with.
my $ready;
my $buf;
my $nbytes;
my $proto = getprotobyname('tcp');
my $iaddr = gethostbyname($host);
my $sin = sockaddr_in( $port, $iaddr );
socket( Socket_Handle, PF_INET, SOCK_STREAM, $proto );
# Make the Socket non-blocking
my $flags = fcntl( Socket_Handle, F_GETFL, 0 ) or do {
$self->do_debug("Can't get flags for socket: $!");
close(Socket_Handle);
next;
};
fcntl( Socket_Handle, F_SETFL, $flags | O_NONBLOCK ) or do {
$self->do_debug("Can't make socket nonblocking: $!");
close(Socket_Handle); next;
};
my $sel = IO::Select->new( \*Socket_Handle );
# connect () and measure the Time.
$t0 = [gettimeofday()];
connect( Socket_Handle, $sin );
($ready) = $sel->can_read($timeout);
$t1 = [gettimeofday()];
if(not defined $ready) {
$self->do_debug("Timeout!");
close(Socket_Handle); next;
}
$nbytes = sysread( Socket_Handle, $buf, 1500 );
if (not defined $nbytes or $nbytes <= 0) {
$self->do_debug("Read nothing and Connection closed!");
close(Socket_Handle); next;
}
# $self->do_debug("Got '$buf' from remote Server");
if (not $buf =~ m/^SSH/) {
$self->do_debug("Not an SSH Server");
close(Socket_Handle); next;
}
($ready) = $sel->can_write($timeout);
if (not defined($ready)) {
$self->do_debug("Huh? Can't write.");
close(Socket_Handle); next;
}
$t2 = [gettimeofday()];
syswrite( Socket_Handle, $greeting . "\n" );
($ready) = $sel->can_read($timeout);
$t3 = [gettimeofday()];
if(not defined $ready) {
$self->do_debug("Timeout!");
close(Socket_Handle); next;
}
close(Socket_Handle);
# We made it! Yeah!
if( $interval eq "connect") {
push @times, tv_interval( $t0, $t1 );
} elsif ( $interval eq "established") {
push @times, tv_interval($t2,$t3);
} elsif ($interval eq "complete") {
push @times, tv_interval($t0,$t3);
} else {
$self->do_debug("You should never see this message.\n The universe will now collapse. Goodbye!\n");
}
}
@times =
map { sprintf "%.10e", $_ } sort { $a <=> $b } grep { $_ ne "-" } @times;
return @times;
}
sub probevars {
my $class = shift;
my $h = $class->SUPER::probevars;
delete $h->{timeout};
return $h;
}
sub targetvars {
my $class = shift;
my $e = "=";
return $class->_makevars($class->SUPER::targetvars, {
greeting => {
_doc => <<DOC,
Greeting string to send to the SSH Server. This will appear in the Logfile.
Use this to make clear, who you are and what you are doing to avoid confusion.
Also, don't use something that is a valid version string. This probe assumes
that the connection gets terminated because of protocol mismatch.
DOC
_default => "SSH-Latency-Measurement-Sorry-for-this-logmessage" ,
},
mininterval => {
_doc => "Minimum interval between the start of two connection attempts in (possibly fractional) seconds.",
_default => 0.5,
_re => '(\d*\.)?\d+',
},
interval => {
_doc => <<DOC,
The interval to be measured. One of:
${e}over
${e}item connect
Interval between connect() and the greeting string from the host.
${e}item established
Interval between our greeting message and the end of the connection
because of Protocol mismatch. This is the default.
${e}item complete
From connect() to the end of the connection.
${e}back
DOC
_sub => sub {
my $interval = shift;
if(not ( $interval eq "connect"
or $interval eq "established"
or $interval eq "complete")) {
return "ERROR: Invalid interval parameter";
}
return undef;
},
_default => 'established',
},
timeout => {
_doc => 'Timeout for the connection.',
_re => '\d+',
_default => 5,
},
port => {
_doc => 'Connect to this port.',
_re => '\d+',
_default => 22,
},
});
}
1;

View File

@@ -0,0 +1,300 @@
package Smokeping::probes::CiscoRTTMonDNS;
=head1 301 Moved Permanently
This is a Smokeping probe module. Please use the command
C<smokeping -man Smokeping::probes::CiscoRTTMonDNS>
to view the documentation or the command
C<smokeping -makepod Smokeping::probes::CiscoRTTMonDNS>
to generate the POD document.
=cut
use strict;
use base qw(Smokeping::probes::basefork);
use Symbol;
use Carp;
use BER;
use SNMP_Session;
use SNMP_util "0.97";
use Smokeping::ciscoRttMonMIB "0.2";
my $e = "=";
sub pod_hash {
return {
name => <<DOC,
Smokeping::probes::CiscoRTTMonDNS.pm - Probe for SmokePing
DOC
description => <<DOC,
A probe for smokeping, which uses the ciscoRttMon MIB functionality ("Service Assurance Agent", "SAA") of Cisco IOS to time ( recursive, type A) DNS queries to a DNS server.
DOC
notes => <<DOC,
${e}head2 host parameter
The host parameter specifies the DNS server, which the router will use.
${e}head2 IOS VERSIONS
This probe only works with IOS 12.0(3)T or higher. It is recommended to test it on less critical routers first.
${e}head2 INSTALLATION
To install this probe copy ciscoRttMonMIB.pm to (\$SMOKEPINGINSTALLDIR)/lib/Smokeping and CiscoRTTMonDNS.pm to (\$SMOKEPINGINSTALLDIR)/lib/Smokeping/probes.
The router(s) must be configured to allow read/write SNMP access. Sufficient is:
snmp-server community RTTCommunity RW
If you want to be a bit more restrictive with SNMP write access to the router, then consider configuring something like this
access-list 2 permit 10.37.3.5
snmp-server view RttMon ciscoRttMonMIB included
snmp-server community RTTCommunity view RttMon RW 2
The above configuration grants SNMP read-write only to 10.37.3.5 (the smokeping host) and only to the ciscoRttMon MIB tree. The probe does not need access to SNMP variables outside the RttMon tree.
DOC
bugs => <<DOC,
The probe does unnecessary DNS queries, i.e. more than configured in the "pings" variable, because the RTTMon MIB only allows to set a total time for all queries in one measurement run (one "life"). Currently the probe sets the life duration to "pings"*5+3 seconds (5 secs is the timeout value hardcoded into this probe).
DOC
see_also => <<DOC,
L<http://oss.oetiker.ch/smokeping/>
L<http://www.switch.ch/misc/leinen/snmp/perl/>
The best source for background info on SAA is Cisco's documentation on L<http://www.cisco.com> and the CISCO-RTTMON-MIB documentation, which is available at:
L<ftp://ftp.cisco.com/pub/mibs/v2/CISCO-RTTMON-MIB.my>
DOC
authors => <<DOC,
Joerg.Kummer at Roche.com
DOC
}
}
my $pingtimeout = 5;
sub new($$$)
{
my $proto = shift;
my $class = ref($proto) || $proto;
my $self = $class->SUPER::new(@_);
# no need for this if we run as a cgi
unless ( $ENV{SERVER_SOFTWARE} ) {
$self->{pingfactor} = 1000;
};
return $self;
}
sub ProbeDesc($){
my $self = shift;
return "CiscoRTTMonDNS.pm";
}
sub pingone ($$) {
my $self = shift;
my $target = shift;
my $name = $target->{vars}{name};
my $pings = $self->pings($target) || 20;
# use the process ID as as row number to make this poll distinct on the router;
my $row=$$;
if (defined
StartRttMibEcho($target->{vars}{ioshost}.":::::2", $target->{addr}, $name,
$pings, $target->{vars}{iosint}, $row))
{
# wait for the series to finish
sleep ($pings*$pingtimeout+5);
if (my @times=FillTimesFromHistoryTable($target->{vars}{ioshost}.":::::2", $pings, $row)){
DestroyData ($target->{vars}{ioshost}.":::::2", $row);
return @times;
}
else {
return();
}
}
else {
return ();
}
}
sub StartRttMibEcho ($$$$$$){
my ($host, $target, $dnsName, $pings, $sourceip, $row) = @_;
# resolve the target name and encode its IP address
$_=$target;
if (!/^([0-9]|\.)+/) {
(my $name, my $aliases, my $addrtype, my $length, my @addrs) = gethostbyname ($target);
$target=join('.',(unpack("C4",$addrs[0])));
}
my @octets=split(/\./,$target);
my $encoded_target= pack ("CCCC", @octets);
# resolve the source name and encode its IP address
my $encoded_source = undef;
if (defined $sourceip) {
$_=$sourceip;
if (!/^([0-9]|\.)+/) {
(my $name, my $aliases, my $addrtype, my $length, my @addrs) = gethostbyname ($sourceip);
$sourceip=join('.',(unpack("C4",$addrs[0])));
}
my @octets=split(/\./,$sourceip);
$encoded_source= pack ("CCCC", @octets);
}
#############################################################
# rttMonCtrlAdminStatus - 1:active 2:notInService 3:notReady 4:createAndGo 5:createAndWait 6:destroy
#delete data from former measurements
#return undef unless defined
# &snmpset($host, "rttMonCtrlAdminStatus.$row",'integer', 6);
#############################################################
# Check RTTMon version and supported protocols
$SNMP_Session::suppress_warnings = 10; # be silent
(my $version)=&snmpget ($host, "rttMonApplVersion");
if (! defined $version ) {
Smokeping::do_log ("$host doesn't support or allow RTTMon !\n");
return undef;
}
Smokeping::do_log ("$host supports $version\n");
$SNMP_Session::suppress_warnings = 0; # report errors
# echo(1), pathEcho(2), fileIO(3), script(4), udpEcho(5), tcpConnect(6), http(7),
# dns(8), jitter(9), dlsw(10), dhcp(11), ftp(12)
my $DnsSupported=0==1;
snmpmaptable ($host,
sub () {
my ($proto, $supported) = @_;
# 1 is true , 2 is false
$DnsSupported=0==0 if ($proto==8 && $supported==1);
},
"rttMonApplSupportedRttTypesValid");
if (! $DnsSupported) {
Smokeping::do_log ("$host doesn't support DNS resolution time measurements !\n");
return undef;
}
#############################################################
#setup the new data row
my @params=();
push @params,
"rttMonCtrlAdminStatus.$row", 'integer', 5,
"rttMonCtrlAdminRttType.$row", 'integer', 8,
"rttMonEchoAdminProtocol.$row", 'integer', 26,
"rttMonEchoAdminNameServer.$row", 'octetstring', $encoded_target,
"rttMonEchoAdminTargetAddressString.$row",'octetstring', $dnsName,
"rttMonCtrlAdminTimeout.$row", 'integer', $pingtimeout*1000,
"rttMonCtrlAdminFrequency.$row", 'integer', $pingtimeout,
"rttMonCtrlAdminNvgen.$row", 'integer', 2,
"rttMonHistoryAdminNumBuckets.$row", 'integer', $pings,
"rttMonHistoryAdminNumLives.$row", 'integer', 1,
"rttMonHistoryAdminFilter.$row", 'integer', 2,
"rttMonScheduleAdminRttStartTime.$row", 'timeticks', 1,
"rttMonScheduleAdminRttLife.$row", 'integer', $pings*$pingtimeout+3,
"rttMonScheduleAdminConceptRowAgeout.$row",'integer', 60;
# the router (or this script) doesn't check whether the IP address is one of
# the router's IP address, i.e. the router might send packets, but never
# gets replies..
if (defined $sourceip) {
push @params, "rttMonEchoAdminSourceAddress.$row", 'octetstring', $encoded_source;
}
return undef unless defined
&snmpset($host, @params);
#############################################################
# and go !
return undef unless defined
&snmpset($host, "rttMonCtrlAdminStatus.$row",'integer',1);
return 1;
}
# RttResponseSense values
# 1:ok 2:disconnected 3:overThreshold 4:timeout 5:busy 6:notConnected 7:dropped 8:sequenceError
# 9:verifyError 10:applicationSpecific 11:dnsServerTimeout 12:tcpConnectTimeout 13:httpTransactionTimeout
#14:dnsQueryError 15:httpError 16:error
sub FillTimesFromHistoryTable($$$$) {
my ($host, $pings, $row) = @_;
my @times;
# snmpmaptable walks two tables (of equal size)
# - "rttMonHistoryCollectionCompletionTime.$row",
# - "rttMonHistoryCollectionSense.$row"
# The code in the sub() argument is executed for each index value snmptable walks
snmpmaptable ($host,
sub () {
my ($index, $rtt, $status) = @_;
push @times, (sprintf ("%.10e", $rtt/1000))
if ($status==1);
},
"rttMonHistoryCollectionCompletionTime.$row",
"rttMonHistoryCollectionSense.$row");
return sort { $a <=> $b } @times;
}
sub DestroyData ($$) {
my ($host, $row) = @_;
&snmpset($host, "rttMonCtrlOperState.$row", 'integer', 3);
&snmpset($host, "rttMonCtrlAdminStatus.$row", 'integer', 2);
#delete any old config
&snmpset($host, "rttMonCtrlAdminStatus.$row", 'integer', 6);
}
sub targetvars {
my $class = shift;
return $class->_makevars($class->SUPER::targetvars, {
_mandatory => [ 'ioshost', 'name' ],
ioshost => {
_doc => <<DOC,
The (mandatory) ioshost parameter specifies the Cisco router, which will send the DNS requests,
as well as the SNMP community string on the router.
DOC
_example => 'RTTcommunity@Myrouter.foobar.com.au',
},
name => {
_doc => "The (mandatory) name parameter is the DNS name to resolve.",
_example => 'www.foobar.com.au',
},
timeout => {
_re => '\d+',
_example => 15,
_default => $pingtimeout+10,
_doc => "How long a single RTTMonDNS 'ping' take at maximum plus 10 seconds to spare. Since we control our own timeout the only purpose of this is to not have us killed by the ping method from basefork.",
},
iosint => {
_doc => <<DOC,
The (optional) iosint parameter is the source address for the DNS packets.
This should be one of the active (!) IP addresses of the router to get
results. IOS looks up the target host address in the forwarding table
and then uses the interface(s) listed there to send the DNS packets. By
default IOS uses the (primary) IP address on the sending interface as
source address for packets originated by the router.
DOC
_example => '10.33.22.11',
},
});
}
=head1
1;

View File

@@ -0,0 +1,334 @@
package Smokeping::probes::CiscoRTTMonEchoICMP;
=head1 301 Moved Permanently
This is a Smokeping probe module. Please use the command
C<smokeping -man Smokeping::probes::CiscoRTTMonEchoICMP>
to view the documentation or the command
C<smokeping -makepod Smokeping::probes::CiscoRTTMonEchoICMP>
to generate the POD document.
=cut
use strict;
use base qw(Smokeping::probes::basefork);
use Symbol;
use Carp;
use BER;
use SNMP_Session;
use SNMP_util "0.97";
use Smokeping::ciscoRttMonMIB "0.2";
sub pod_hash {
my $e = "=";
return {
name => <<DOC,
Smokeping::probes::CiscoRTTMonEchoICMP - Probe for SmokePing
DOC
description => <<DOC,
A probe for smokeping, which uses the ciscoRttMon MIB functionality ("Service Assurance Agent", "SAA") of Cisco IOS to measure ICMP echo ("ping") roundtrip times between a Cisco router and any IP address.
DOC
notes => <<DOC,
${e}head2 IOS VERSIONS
It is highly recommended to use this probe with routers running IOS 12.0(3)T or higher and to test it on less critical routers first. I managed to crash a router with 12.0(9) quite consistently ( in IOS lingo 12.0(9) is older code than 12.0(3)T ). I did not observe crashes on higher IOS releases, but messages on the router like the one below, when multiple processes concurrently accessed the same router (this case was IOS 12.1(12b) ):
Aug 20 07:30:14: %RTT-3-SemaphoreBadUnlock: %RTR: Attempt to unlock semaphore by wrong RTR process 70, locked by 78
Aug 20 07:35:15: %RTT-3-SemaphoreInUse: %RTR: Could not obtain a lock for RTR. Process 80
${e}head2 INSTALLATION
To install this probe copy ciscoRttMonMIB.pm files to (\$SMOKEPINGINSTALLDIR)/lib/Smokeping and CiscoRTTMonEchoICMP.pm to (\$SMOKEPINGINSTALLDIR)/lib/Smokeping/probes. V0.97 or higher of Simon Leinen's SNMP_Session.pm is required.
The router(s) must be configured to allow read/write SNMP access. Sufficient is:
snmp-server community RTTCommunity RW
If you want to be a bit more restrictive with SNMP write access to the router, then consider configuring something like this
access-list 2 permit 10.37.3.5
snmp-server view RttMon ciscoRttMonMIB included
snmp-server community RTTCommunity view RttMon RW 2
The above configuration grants SNMP read-write only to 10.37.3.5 (the smokeping host) and only to the ciscoRttMon MIB tree. The probe does not need access to SNMP variables outside the RttMon tree.
DOC
bugs => <<DOC,
The probe sends unnecessary pings, i.e. more than configured in the "pings" variable, because the RTTMon MIB only allows to set a total time for all pings in one measurement run (one "life"). Currently the probe sets the life duration to "pings"*5+3 seconds (5 secs is the ping timeout value hardcoded into this probe).
DOC
see_also => <<DOC,
L<http://oss.oetiker.ch/smokeping/>
L<http://www.switch.ch/misc/leinen/snmp/perl/>
The best source for background info on SAA is Cisco's documentation on L<http://www.cisco.com> and the CISCO-RTTMON-MIB documentation, which is available at:
L<ftp://ftp.cisco.com/pub/mibs/v2/CISCO-RTTMON-MIB.my>
DOC
authors => <<DOC,
Joerg.Kummer at Roche.com
DOC
}
}
my $pingtimeout = 5;
sub new($$$)
{
my $proto = shift;
my $class = ref($proto) || $proto;
my $self = $class->SUPER::new(@_);
# no need for this if we run as a cgi
unless ( $ENV{SERVER_SOFTWARE} ) {
$self->{pingfactor} = 1000;
};
return $self;
}
sub ProbeDesc($){
my $self = shift;
return "CiscoRTTMonEchoICMP";
}
sub pingone ($$) {
my $self = shift;
my $target = shift;
my $pings = $self->pings($target) || 20;
my $tos = $target->{vars}{tos};
my $bytes = $target->{vars}{packetsize};
# use the process ID as as row number to make this poll distinct on the router;
my $row=$$;
if (defined
StartRttMibEcho($target->{vars}{ioshost}.":::::2", $target->{addr},
$bytes, $pings, $target->{vars}{iosint}, $tos, $row, $target->{vars}{vrf}))
{
# wait for the series to finish
sleep ($pings*$pingtimeout+5);
if (my @times=FillTimesFromHistoryTable($target->{vars}{ioshost}.":::::2", $pings, $row)){
DestroyData ($target->{vars}{ioshost}.":::::2", $row);
return @times;
}
else {
return();
}
}
else {
return ();
}
}
sub StartRttMibEcho ($$$$$$){
my ($host, $target, $size, $pings, $sourceip, $tos, $row,$vrf) = @_;
# resolve the target name and encode its IP address
$_=$target;
if (!/^([0-9]|\.)+/) {
(my $name, my $aliases, my $addrtype, my $length, my @addrs) = gethostbyname ($target);
$target=join('.',(unpack("C4",$addrs[0])));
}
my @octets=split(/\./,$target);
my $encoded_target= pack ("CCCC", @octets);
# resolve the source name and encode its IP address
my $encoded_source = undef;
if (defined $sourceip) {
$_=$sourceip;
if (!/^([0-9]|\.)+/) {
(my $name, my $aliases, my $addrtype, my $length, my @addrs) = gethostbyname ($sourceip);
$sourceip=join('.',(unpack("C4",$addrs[0])));
}
my @octets=split(/\./,$sourceip);
$encoded_source= pack ("CCCC", @octets);
}
#############################################################
# rttMonCtrlAdminStatus - 1:active 2:notInService 3:notReady 4:createAndGo 5:createAndWait 6:destroy
#delete data from former measurements
#return undef unless defined
# &snmpset($host, "rttMonCtrlAdminStatus.$row",'integer', 6);
#############################################################
# Check RTTMon version and supported protocols
$SNMP_Session::suppress_warnings = 10; # be silent
(my $version)=&snmpget ($host, "rttMonApplVersion");
if (! defined $version ) {
Smokeping::do_log ("$host doesn't support or allow RTTMon !\n");
return undef;
}
Smokeping::do_log ("$host supports $version\n");
$SNMP_Session::suppress_warnings = 0; # report errors
# echo(1), pathEcho(2), fileIO(3), script(4), udpEcho(5), tcpConnect(6), http(7),
# dns(8), jitter(9), dlsw(10), dhcp(11), ftp(12)
my $udpEchoSupported=0==1;
snmpmaptable ($host,
sub () {
my ($proto, $supported) = @_;
# 1 is true , 2 is false
$udpEchoSupported=0==0 if ($proto==5 && $supported==1);
},
"rttMonApplSupportedRttTypesValid");
#############################################################
#setup the new data row
my @params=();
push @params,
"rttMonCtrlAdminStatus.$row", 'integer', 5,
"rttMonCtrlAdminRttType.$row", 'integer', 1,
"rttMonEchoAdminProtocol.$row", 'integer', 2,
"rttMonEchoAdminTargetAddress.$row", 'octetstring', $encoded_target,
"rttMonCtrlAdminTimeout.$row", 'integer', $pingtimeout*1000,
"rttMonCtrlAdminFrequency.$row", 'integer', $pingtimeout,
"rttMonHistoryAdminNumBuckets.$row", 'integer', $pings,
"rttMonHistoryAdminNumLives.$row", 'integer', 1,
"rttMonHistoryAdminFilter.$row", 'integer', 2,
"rttMonEchoAdminPktDataRequestSize.$row",'integer', $size-8,
"rttMonScheduleAdminRttStartTime.$row", 'timeticks', 1,
"rttMonScheduleAdminRttLife.$row", 'integer', $pings*$pingtimeout+3,
"rttMonScheduleAdminConceptRowAgeout.$row",'integer', 60;
if(defined($vrf)){
push @params,"rttMonEchoAdminVrfName.$row",'octetstring',$vrf;
}
# with udpEcho support (>= 12.0(3)T ) the ICMP ping support was enhanced in the RTTMon SW - we are
# NOT using udpEcho, but echo (ICMP echo, ping)
if ($udpEchoSupported) {
push @params, "rttMonEchoAdminTOS.$row", 'integer', $tos;
push @params, "rttMonCtrlAdminNvgen.$row", 'integer', 2;
# the router (or this script) doesn't check whether the IP address is one of
# the router's IP address, i.e. the router might send packets, but never
# gets ping replies..
if (defined $sourceip) {
push @params, "rttMonEchoAdminSourceAddress.$row", 'octetstring', $encoded_source;
}
}
else {
Smokeping::do_log ("Warning this host does not support ToS or iosint\n");
}
return undef unless defined
&snmpset($host, @params);
#############################################################
# and go !
return undef unless defined
&snmpset($host, "rttMonCtrlAdminStatus.$row",'integer',1);
return 1;
}
# RttResponseSense values
# 1:ok 2:disconnected 3:overThreshold 4:timeout 5:busy 6:notConnected 7:dropped 8:sequenceError
# 9:verifyError 10:applicationSpecific 11:dnsServerTimeout 12:tcpConnectTimeout 13:httpTransactionTimeout
#14:dnsQueryError 15:httpError 16:error
sub FillTimesFromHistoryTable($$$$) {
my ($host, $pings, $row) = @_;
my @times;
# snmpmaptable walks two tables (of equal size)
# - "rttMonHistoryCollectionCompletionTime.$row",
# - "rttMonHistoryCollectionSense.$row"
# The code in the sub() argument is executed for each index value snmptable walks
snmpmaptable ($host,
sub () {
my ($index, $rtt, $status) = @_;
push @times, (sprintf ("%.10e", $rtt/1000))
if ($status==1);
},
"rttMonHistoryCollectionCompletionTime.$row",
"rttMonHistoryCollectionSense.$row");
return sort { $a <=> $b } @times;
}
sub DestroyData ($$) {
my ($host, $row) = @_;
&snmpset($host, "rttMonCtrlOperState.$row", 'integer', 3);
&snmpset($host, "rttMonCtrlAdminStatus.$row", 'integer', 2);
#delete any old config
&snmpset($host, "rttMonCtrlAdminStatus.$row", 'integer', 6);
}
sub targetvars {
my $class = shift;
return $class->_makevars($class->SUPER::targetvars, {
_mandatory => [ 'ioshost' ],
ioshost => {
_example => 'RTTcommunity@Myrouter.foobar.com.au',
_doc => <<DOC,
The (mandatory) ioshost parameter specifies the Cisco router, which will
execute the pings, as well as the SNMP community string on the router.
DOC
},
timeout => {
_re => '\d+',
_example => 15,
_default => $pingtimeout+10,
_doc => "How long a single RTTMonEcho ICMP 'ping' take at maximum plus 10 seconds to spare. Since we control our own timeout the only purpose of this is to not have us killed by the ping method from basefork.",
},
iosint => {
_example => '10.33.22.11',
_doc => <<DOC,
The (optional) iosint parameter is the source address for the pings
sent. This should be one of the active (!) IP addresses of the router to
get results. IOS looks up the target host address in the forwarding table
and then uses the interface(s) listed there to send the ping packets. By
default IOS uses the (primary) IP address on the sending interface as
source address for a ping. The RTTMon MIB versions before IOS 12.0(3)T
didn't support this parameter.
DOC
},
tos => {
_example => 160,
_default => 0,
_doc => <<DOC,
The (optional) tos parameter specifies the value of the ToS byte in
the IP header of the pings. Multiply DSCP values times 4 and Precedence
values times 32 to calculate the ToS values to configure, e.g. ToS 160
corresponds to a DSCP value 40 and a Precedence value of 5. The RTTMon
MIB versions before IOS 12.0(3)T didn't support this parameter.
DOC
},
vrf => {
_example => "INTERNET",
_doc => <<DOC,
The the VPN name in which the RTT operation will be used. For regular RTT
operation this field should not be configured. The agent
will use this field to identify the VPN routing Table for
this operation.
DOC
},
packetsize => {
_doc => <<DOC,
The packetsize parameter lets you configure the packetsize for the pings
sent. The minimum is 8, the maximum 16392. Use the same number as with
fping, if you want the same packet sizes being used on the network.
DOC
_default => 56,
_re => '\d+',
_sub => sub {
my $val = shift;
return "ERROR: packetsize must be between 8 and 16392"
unless $val >= 8 and $val <= 16392;
return undef;
}
}
});
}
1;

View File

@@ -0,0 +1,311 @@
package Smokeping::probes::CiscoRTTMonTcpConnect;
=head1 301 Moved Permanently
This is a Smokeping probe module. Please use the command
C<smokeping -man Smokeping::probes::CiscoRTTMonTcpConnect>
to view the documentation or the command
C<smokeping -makepod Smokeping::probes::CiscoRTTMonTcpConnect>
to generate the POD document.
=cut
use strict;
use base qw(Smokeping::probes::basefork);
use Symbol;
use Carp;
use BER;
use SNMP_Session;
use SNMP_util "0.97";
use Smokeping::ciscoRttMonMIB "0.2";
sub pod_hash {
my $e = "=";
return {
name => <<DOC,
Smokeping::probes::CiscoRTTMonTcpConnect - Probe for SmokePing
DOC
description => <<DOC,
A probe for smokeping, which uses the ciscoRttMon MIB functionality ("Service Assurance Agent", "SAA") of Cisco IOS to measure TCP connect times between a Cisco router and a TCP server. The measured value is the time is the time to establish a TCP session, i.e. the time between the initial "SYN" TCP packet of the router and the "SYN ACK" packet of the host. The router terminates the TCP session immediately after the reception of "SYN ACK" with a "FIN" packet.
DOC
notes => <<DOC,
${e}head2 IOS VERSIONS
This probe only works with Cisco IOS 12.0(3)T or higher. It is recommended to test it on less critical routers first.
${e}head2 INSTALLATION
To install this probe copy ciscoRttMonMIB.pm to (\$SMOKEPINGINSTALLDIR)/Smokeping/lib and CiscoRTTMonTcpConnect.pm to (\$SMOKEPINGINSTALLDIR)/lib/Smokeping/probes. V0.97 or higher of Simon Leinen's SNMP_Session.pm is required.
The router(s) must be configured to allow read/write SNMP access. Sufficient is:
snmp-server community RTTCommunity RW
If you want to be a bit more restrictive with SNMP write access to the router, then consider configuring something like this
access-list 2 permit 10.37.3.5
snmp-server view RttMon ciscoRttMonMIB included
snmp-server community RTTCommunity view RttMon RW 2
The above configuration grants SNMP read-write only to 10.37.3.5 (the smokeping host) and only to the ciscoRttMon MIB tree. The probe does not need access to SNMP variables outside the RttMon tree.
DOC
bugs => <<DOC,
The probe establishes unnecessary connections, i.e. more than configured in the "pings" variable, because the RTTMon MIB only allows to set a total time for all connections in one measurement run (one "life"). Currently the probe sets the life duration to "pings"*5+3 seconds (5 secs is the timeout value hardcoded into this probe).
DOC
see_also => <<DOC,
L<http://oss.oetiker.ch/smokeping/>
L<http://www.switch.ch/misc/leinen/snmp/perl/>
The best source for background info on SAA is Cisco's documentation on L<http://www.cisco.com> and the CISCO-RTTMON-MIB documentation, which is available at:
L<ftp://ftp.cisco.com/pub/mibs/v2/CISCO-RTTMON-MIB.my>
DOC
authors => <<DOC,
Joerg.Kummer at Roche.com
DOC
}
}
my $pingtimeout = 5;
sub new($$$)
{
my $proto = shift;
my $class = ref($proto) || $proto;
my $self = $class->SUPER::new(@_);
# no need for this if we run as a cgi
unless ( $ENV{SERVER_SOFTWARE} ) {
$self->{pingfactor} = 1000;
};
return $self;
}
sub ProbeDesc($){
my $self = shift;
return "CiscoRTTMonTcpConnect";
}
sub pingone ($$) {
my $self = shift;
my $target = shift;
my $pings = $self->pings($target) || 20;
my $tos = $target->{vars}{tos};
my $port = $target->{vars}{port};
# use the process ID as as row number to make this poll distinct on the router;
my $row=$$;
if (defined
StartRttMibEcho($target->{vars}{ioshost}.":::::2", $target->{addr}, $port,
$pings, $target->{vars}{iosint}, $tos, $row))
{
# wait for the series to finish
sleep ($pings*$pingtimeout+5);
if (my @times=FillTimesFromHistoryTable($target->{vars}{ioshost}.":::::2", $pings, $row)){
DestroyData ($target->{vars}{ioshost}.":::::2", $row);
return @times;
}
else {
return();
}
}
else {
return ();
}
}
sub StartRttMibEcho ($$$$$$){
my ($host, $target, $port, $pings, $sourceip, $tos, $row) = @_;
# resolve the target name and encode its IP address
$_=$target;
if (!/^([0-9]|\.)+/) {
(my $name, my $aliases, my $addrtype, my $length, my @addrs) = gethostbyname ($target);
$target=join('.',(unpack("C4",$addrs[0])));
}
my @octets=split(/\./,$target);
my $encoded_target= pack ("CCCC", @octets);
# resolve the source name and encode its IP address
my $encoded_source = undef;
if (defined $sourceip) {
$_=$sourceip;
if (!/^([0-9]|\.)+/) {
(my $name, my $aliases, my $addrtype, my $length, my @addrs) = gethostbyname ($sourceip);
$sourceip=join('.',(unpack("C4",$addrs[0])));
}
my @octets=split(/\./,$sourceip);
$encoded_source= pack ("CCCC", @octets);
}
#############################################################
# rttMonCtrlAdminStatus - 1:active 2:notInService 3:notReady 4:createAndGo 5:createAndWait 6:destroy
#delete data from former measurements
#return undef unless defined
# &snmpset($host, "rttMonCtrlAdminStatus.$row",'integer', 6);
#############################################################
# Check RTTMon version and supported protocols
$SNMP_Session::suppress_warnings = 10; # be silent
(my $version)=&snmpget ($host, "rttMonApplVersion");
if (! defined $version ) {
Smokeping::do_log ("$host doesn't support or allow RTTMon !\n");
return undef;
}
Smokeping::do_log ("$host supports $version\n");
$SNMP_Session::suppress_warnings = 0; # report errors
# echo(1), pathEcho(2), fileIO(3), script(4), udpEcho(5), tcpConnect(6), http(7),
# dns(8), jitter(9), dlsw(10), dhcp(11), ftp(12)
my $tcpConnSupported=0==1;
snmpmaptable ($host,
sub () {
my ($proto, $supported) = @_;
# 1 is true , 2 is false
$tcpConnSupported=0==0 if ($proto==6 && $supported==1);
},
"rttMonApplSupportedRttTypesValid");
if (! $tcpConnSupported) {
Smokeping::do_log ("$host doesn't support TCP connection time measurements !\n");
return undef;
}
#############################################################
#setup the new data row
my @params=();
push @params,
"rttMonCtrlAdminStatus.$row", 'integer', 5,
"rttMonCtrlAdminRttType.$row", 'integer', 6,
"rttMonEchoAdminProtocol.$row", 'integer', 24,
"rttMonEchoAdminTargetAddress.$row", 'octetstring', $encoded_target,
"rttMonEchoAdminTargetPort.$row", 'integer', $port,
"rttMonCtrlAdminTimeout.$row", 'integer', $pingtimeout*1000,
"rttMonCtrlAdminFrequency.$row", 'integer', $pingtimeout,
"rttMonEchoAdminControlEnable.$row", 'integer', 2,
"rttMonEchoAdminTOS.$row", 'integer', $tos,
"rttMonCtrlAdminNvgen.$row", 'integer', 2,
"rttMonHistoryAdminNumBuckets.$row", 'integer', $pings,
"rttMonHistoryAdminNumLives.$row", 'integer', 1,
"rttMonHistoryAdminFilter.$row", 'integer', 2,
"rttMonScheduleAdminRttStartTime.$row", 'timeticks', 1,
"rttMonScheduleAdminRttLife.$row", 'integer', $pings*$pingtimeout+3,
"rttMonScheduleAdminConceptRowAgeout.$row",'integer', 60;
# the router (or this script) doesn't check whether the IP address is one of
# the router's IP address, i.e. the router might send packets, but never
# gets replies..
if (defined $sourceip) {
push @params, "rttMonEchoAdminSourceAddress.$row", 'octetstring', $encoded_source;
}
return undef unless defined
&snmpset($host, @params);
#############################################################
# and go !
return undef unless defined
&snmpset($host, "rttMonCtrlAdminStatus.$row",'integer',1);
return 1;
}
# RttResponseSense values
# 1:ok 2:disconnected 3:overThreshold 4:timeout 5:busy 6:notConnected 7:dropped 8:sequenceError
# 9:verifyError 10:applicationSpecific 11:dnsServerTimeout 12:tcpConnectTimeout 13:httpTransactionTimeout
#14:dnsQueryError 15:httpError 16:error
sub FillTimesFromHistoryTable($$$$) {
my ($host, $pings, $row) = @_;
my @times;
# snmpmaptable walks two columns of rttMonHistoryCollectionTable
# - "rttMonHistoryCollectionCompletionTime.$row",
# - "rttMonHistoryCollectionSense.$row"
# The code in the sub() argument is executed for each index value snmptable walks
snmpmaptable ($host,
sub () {
my ($index, $rtt, $status) = @_;
push @times, (sprintf ("%.10e", $rtt/1000))
if ($status==1);
},
"rttMonHistoryCollectionCompletionTime.$row",
"rttMonHistoryCollectionSense.$row");
return sort { $a <=> $b } @times;
}
sub DestroyData ($$) {
my ($host, $row) = @_;
&snmpset($host, "rttMonCtrlOperState.$row", 'integer', 3);
&snmpset($host, "rttMonCtrlAdminStatus.$row", 'integer', 2);
#delete any old config
&snmpset($host, "rttMonCtrlAdminStatus.$row", 'integer', 6);
}
sub targetvars {
my $class = shift;
return $class->_makevars($class->SUPER::targetvars, {
_mandatory => [ 'ioshost' ],
ioshost => {
_example => 'RTTcommunity@Myrouter.foobar.com.au',
_doc => <<DOC,
The (mandatory) ioshost parameter specifies the Cisco router, which will
establish the TCP connections as well as the SNMP community string on
the router.
DOC
},
port => {
_default => 80,
_re => '\d+',
_doc => <<DOC,
The (optional) port parameter lets you configure the destination TCP
port on the host. The default is the http port 80.
DOC
},
timeout => {
_re => '\d+',
_example => 15,
_default => $pingtimeout+10,
_doc => "How long a single RTTMon TcpConnect 'ping' take at maximum plus 10 seconds to spare. Since we control our own timeout the only purpose of this is to not have us killed by the ping method from basefork.",
},
iosint => {
_example => '10.33.22.11',
_doc => <<DOC,
The (optional) iosint parameter is the source address for the TCP
connections. This should be one of the active (!) IP addresses of the
router to get results. IOS looks up the target host address in the
forwarding table and then uses the interface(s) listed there to send
the TCP packets. By default IOS uses the (primary) IP address on the
sending interface as source address for a connection.
DOC
},
tos => {
_default => 0,
_example => 160,
_re => '\d+',
_doc => <<DOC,
The (optional) tos parameter specifies the value of the ToS byte in the
IP header of the packets from the router. Multiply DSCP values times 4
and Precedence values times 32 to calculate the ToS values to configure,
e.g. ToS 160 corresponds to a DSCP value 40 and a Precedence value of
5. Please note that this will not influence the ToS value in the packets
sent by the the host.
DOC
},
});
}
1;

View File

@@ -0,0 +1,352 @@
package Smokeping::probes::Curl;
=head1 301 Moved Permanently
This is a Smokeping probe module. Please use the command
C<smokeping -man Smokeping::probes::Curl>
to view the documentation or the command
C<smokeping -makepod Smokeping::probes::Curl>
to generate the POD document.
=cut
use strict;
use base qw(Smokeping::probes::basefork);
use Carp;
my $DEFAULTBIN = "/usr/bin/curl";
sub pod_hash {
return {
name => "Smokeping::probes::Curl - a curl(1) probe for SmokePing",
overview => "Fetches an HTTP or HTTPS URL using curl(1).",
description => "(see curl(1) for details of the options below)",
authors => <<'DOC',
Gerald Combs <gerald [AT] ethereal.com>
Niko Tyni <ntyni@iki.fi>
DOC
notes => <<DOC,
You should consider setting a lower value for the C<pings> variable than the
default 20, as repetitive URL fetching may be quite heavy on the server.
The URL to be tested used to be specified by the variable 'url' in earlier
versions of Smokeping, and the 'host' setting did not influence it in any
way. The variable name has now been changed to 'urlformat', and it can
(and in most cases should) contain a placeholder for the 'host' variable.
DOC
see_also => "curl(1), L<http://curl.haxx.se/>",
}
}
sub probevars {
my $class = shift;
my $h = $class->SUPER::probevars;
delete $h->{timeout};
return $class->_makevars($h, {
binary => {
_doc => "The location of your curl binary.",
_default => $DEFAULTBIN,
_sub => sub {
my $val = shift;
return "ERROR: Curl 'binary' $val does not point to an executable"
unless -f $val and -x _;
return undef;
},
},
});
}
sub targetvars {
my $class = shift;
return $class->_makevars($class->SUPER::targetvars, {
_mandatory => [ 'urlformat' ],
agent => {
_doc => <<DOC,
The "-A" curl(1) option. This is a full HTTP User-Agent header including
the words "User-Agent:". Note that it does not need any quotes around it.
DOC
_example => 'User-Agent: Lynx/2.8.4rel.1 libwww-FM/2.14 SSL-MM/1.4.1 OpenSSL/0.9.6c',
_sub => sub {
my $val = shift;
return "The Curl 'agent' string does not need any quotes around it anymore."
if $val =~ /^["']/ or $val =~ /["']$/;
return undef;
},
},
timeout => {
_doc => qq{The "-m" curl(1) option. Maximum timeout in seconds.},
_re => '\d+',
_example => 20,
_default => 10,
},
interface => {
_doc => <<DOC,
The "--interface" curl(1) option. Bind to a specific interface, IP address or
host name.
DOC
_example => 'eth0',
},
ssl2 => {
_doc => qq{The "-2" curl(1) option. Force SSL2.},
_example => 1,
},
urlformat => {
_doc => <<DOC,
The template of the URL to fetch. Can be any one that curl supports.
Any occurrence of the string '%host%' will be replaced with the
host to be probed.
DOC
_example => "http://%host%/",
},
insecure_ssl => {
_doc => <<DOC,
The "-k" curl(1) option. Accept SSL connections that don't have a secure
certificate chain to a trusted CA. Note that if you are going to monitor
https targets, you'll probably have to either enable this option or specify
the CA path to curl through extraargs below. For more info, see the
curl(1) manual page.
DOC
_example => 1,
},
extrare=> {
_doc => <<DOC,
The regexp used to split the extraargs string into an argument list,
in the "/regexp/" notation. This contains just the space character
(" ") by default, but if you need to specify any arguments containing spaces,
you can set this variable to a different value.
DOC
_default => "/ /",
_example => "/ /",
_sub => sub {
my $val = shift;
return "extrare should be specified in the /regexp/ notation"
unless $val =~ m,^/.*/$,;
return undef;
},
},
follow_redirects => {
_doc => <<DOC,
If this variable is set to 'yes', curl will follow any HTTP redirection steps (the '-L' option).
If set to 'no', HTTP Location: headers will not be followed. See also 'include_redirects'.
DOC
_default => "no",
_re => "(yes|no)",
_example => "yes",
},
include_redirects => {
_doc => <<DOC,
If this variable is set to 'yes', the measurement result will include the time
spent on following any HTTP redirection steps. If set to 'no', only the last
step is measured. See also 'follow_redirects'.
DOC
_default => "no",
_re => "(yes|no)",
_example => "yes",
},
extraargs => {
_doc => <<DOC,
Any extra arguments you might want to hand to curl(1). The arguments
should be separated by the regexp specified in "extrare", which
contains just the space character (" ") by default.
Note that curl will be called with the resulting list of arguments
without any shell expansion. If you need to specify any arguments
containing spaces, you should set "extrare" to something else.
As a complicated example, to explicitly set the "Host:" header in Curl
requests, you need to set "extrare" to something else, eg. "/;/",
and then specify C<extraargs = --header;Host: www.example.com>.
DOC
_example => "-6 --head --user user:password",
},
expect => {
_doc => <<DOC,
Require the given text to appear somewhere in the response, otherwise
probe is treated as a failure
DOC
_default => "",
_example => "Status: green",
},
require_zero_status => {
_doc => <<DOC,
If this variable is set to 'yes', responses will only be counted if
Curl's exit status is '0'. This is useful for reporting timeouts as
losses rather than delayed responses.
DOC
_default => "no",
_re => "(yes|no)",
_example => "yes",
},
});
}
# derived class will mess with this through the 'features' method below
my $featurehash = {
agent => "-A",
timeout => "-m",
interface => "--interface",
};
sub features {
my $self = shift;
my $newval = shift;
$featurehash = $newval if defined $newval;
return $featurehash;
}
sub new {
my $proto = shift;
my $class = ref($proto) || $proto;
my $self = $class->SUPER::new(@_);
$self->_init if $self->can('_init');
# no need for this if running as a CGI
$self->test_usage unless $ENV{SERVER_SOFTWARE};
return $self;
}
# warn about unsupported features
sub test_usage {
my $self = shift;
my $bin = $self->{properties}{binary};
my @unsupported;
my $arghashref = $self->features;
my %arghash = %$arghashref;
my $curl_man = `$bin --help all`;
for my $feature (keys %arghash) {
next if $curl_man =~ /\Q$arghash{$feature}/;
push @unsupported, $feature;
$self->do_log("Note: your curl doesn't support the $feature feature (option $arghash{$feature}), disabling it");
}
map { delete $arghashref->{$_} } @unsupported;
# if ($curl_man !~ /\stime_redirect\s/) {
# $self->do_log("Note: your curl doesn't support the 'time_redirect' output variable; 'include_redirects' will not function.");
# }
return;
}
sub ProbeDesc($) {
return "URLs using curl(1)";
}
# other than host, count and protocol-specific args come from here
sub make_args {
my $self = shift;
my $target = shift;
my @args;
my %arghash = %{$self->features};
for (keys %arghash) {
my $val = $target->{vars}{$_};
push @args, ($arghash{$_}, $val) if defined $val;
}
return @args;
}
# This is what derived classes will override
sub proto_args {
my $self = shift;
my $target = shift;
# XXX - It would be neat if curl had a "time_transfer". For now,
# we take the total time minus the DNS lookup time.
my @args = ("-w", "Time: %{time_total} DNS time: %{time_namelookup} Redirect time: %{time_redirect}\\n");
my $ssl2 = $target->{vars}{ssl2};
push (@args, "-2") if $ssl2;
my $insecure_ssl = $target->{vars}{insecure_ssl};
push (@args, '-k') if $insecure_ssl;
my $follow = $target->{vars}{follow_redirects};
push (@args, '-L') if $follow eq "yes";
return(@args);
}
sub extra_args {
my $self = shift;
my $target = shift;
my $args = $target->{vars}{extraargs};
return () unless defined $args;
my $re = $target->{vars}{extrare};
($re =~ m,^/(.*)/$,) and $re = qr{$1};
return split($re, $args);
}
sub make_commandline {
my $self = shift;
my $target = shift;
my $count = shift;
my @args = $self->make_args($target);
my $url = $target->{vars}{urlformat};
my $host = $target->{addr};
$url =~ s/%host%/$host/g;
my @urls = split(/\s+/, $url);
# push @args, ("-o", "/dev/null") for (@urls);
push @args, $self->proto_args($target);
push @args, $self->extra_args($target);
return ($self->{properties}{binary}, @args, @urls);
}
sub pingone {
my $self = shift;
my $t = shift;
my @cmd = $self->make_commandline($t);
$self->do_debug("executing command list " . join(",", map { qq('$_') } @cmd));
my @times;
my $count = $self->pings($t);
for (my $i = 0 ; $i < $count; $i++) {
open(P, "-|") or exec @cmd;
my $val;
my $expectOK = 1;
$expectOK = 0 if ($t->{vars}{expect} ne "");
while (<P>) {
chomp;
if (!$expectOK and index($_, $t->{vars}{expect}) != -1) {
$expectOK = 1;
}
/Time: (\d+\.\d+) DNS time: (\d+\.\d+) Redirect time: (\d+\.\d+)?/ and do {
$val += $1 - $2;
if ($t->{vars}{include_redirects} eq "yes" and defined $3) {
$val += $3;
}
$self->do_debug("curl output: '$_', result: $val");
};
}
close P;
if ($?) {
my $status = $? >> 8;
my $signal = $? & 127;
my $why = "with status $status";
$why .= " [signal $signal]" if $signal;
# only log warnings on the first ping of the first ping round
my $function = ($self->rounds_count == 1 and $i == 0) ?
"do_log" : "do_debug";
$self->$function(qq(WARNING: curl exited $why on $t->{addr}));
}
if ($? == 0 or $t->{vars}{require_zero_status} eq "no") {
push @times, $val if (defined $val and $expectOK);
}
}
# carp("Got @times") if $self->debug;
return sort { $a <=> $b } @times;
}
1;

View File

@@ -0,0 +1,142 @@
package Smokeping::probes::DNS;
=head1 301 Moved Permanently
This is a Smokeping probe module. Please use the command
C<smokeping -man Smokeping::probes::DNS>
to view the documentation or the command
C<smokeping -makepod Smokeping::probes::DNS>
to generate the POD document.
=cut
use strict;
use base qw(Smokeping::probes::basefork);
use IPC::Open3;
use Symbol;
use Carp;
sub pod_hash {
return {
name => <<DOC,
Smokeping::probes::DNS - Name Service Probe for SmokePing
DOC
description => <<DOC,
Integrates dig as a probe into smokeping. The variable B<binary> must
point to your copy of the dig program. If it is not installed on
your system yet, you should install bind-utils >= 9.0.0.
The Probe asks the given host n-times for it's name. Where n is
the amount specified in the config File.
DOC
authors => <<'DOC',
Igor Petrovski <pigor@myrealbox.com>,
Carl Elkins <carl@celkins.org.uk>,
Andre Stolze <stolze@uni-muenster.de>,
Niko Tyni <ntyni@iki.fi>,
Chris Poetzel<cpoetzel@anl.gov>
DOC
};
}
my $dig_re=qr/query time:\s+([0-9.]+)\smsec.*/i;
sub new($$$)
{
my $proto = shift;
my $class = ref($proto) || $proto;
my $self = $class->SUPER::new(@_);
# no need for this if we run as a cgi
unless ( $ENV{SERVER_SOFTWARE} ) {
my $call = "$self->{properties}{binary} localhost";
my $return = `$call 2>&1`;
if ($return =~ m/$dig_re/s){
$self->{pingfactor} = 1000;
print "### parsing dig output...OK\n";
} else {
croak "ERROR: output of '$call' does not match $dig_re\n";
}
};
return $self;
}
sub probevars {
my $class = shift;
return $class->_makevars($class->SUPER::probevars, {
_mandatory => [ 'binary' ],
binary => {
_doc => "The location of your dig binary.",
_example => '/usr/bin/dig',
_sub => sub {
my $val = shift;
return "ERROR: DNS 'binary' does not point to an executable"
unless -f $val and -x _;
return undef;
},
},
});
}
sub targetvars {
my $class = shift;
return $class->_makevars($class->SUPER::targetvars, {
lookup => { _doc => "Name of the host to look up in the dns.",
_example => "www.example.org",
},
server => { _doc => "Name of the dns server to use.",
_example => "ns1.someisp.net",
},
});
}
sub ProbeDesc($){
my $self = shift;
return "DNS requests";
}
sub pingone ($){
my $self = shift;
my $target = shift;
my $inh = gensym;
my $outh = gensym;
my $errh = gensym;
my $host = $target->{addr};
my $lookuphost = $target->{vars}{lookup};
$lookuphost = $target->{addr} unless defined $lookuphost;
my $dnsserver = $target->{vars}{server} || $host;
my $query = "$self->{properties}{binary} \@$dnsserver $lookuphost";
my @times;
$self->do_debug("query=$query\n");
for (my $run = 0; $run < $self->pings($target); $run++) {
my $pid = open3($inh,$outh,$errh, $query);
while (<$outh>) {
if (/$dig_re/i) {
push @times, $1;
last;
}
}
waitpid $pid,0;
my $rc = $?;
carp "$query returned with exit code $rc. run with debug enabled to get more information" unless $rc == 0;
close $errh;
close $inh;
close $outh;
}
@times = map {sprintf "%.10e", $_ / $self->{pingfactor}} sort {$a <=> $b} grep {$_ ne "-"} @times;
# $self->do_debug("time=@times\n");
return @times;
}
1;

View File

@@ -0,0 +1,451 @@
package Smokeping::probes::DismanPing;
=head1 301 Moved Permanently
This is a Smokeping probe module. Please use the command
C<smokeping -man Smokeping::probes::DismanPing>
to view the documentation or the command
C<smokeping -makepod Smokeping::probes::DismanPing>
to generate the POD document.
=cut
use strict;
use base qw(Smokeping::probes::basevars);
use SNMP_Session "1.13";
use SNMP_util "1.13";
use Smokeping::pingMIB "0.1";
use Socket;
use Net::Domain qw(hostname);
sub pod_hash {
my $e = "=";
return {
name => <<DOC,
Smokeping::probes::DismanPing - DISMAN-PING-MIB Probe for SmokePing
DOC
description => <<DOC,
Uses the DISMAN-PING-MIB to cause a remote system to send probes.
DOC
authors => <<DOC,
Bill Fenner <fenner\@research.att.com>,
Tobi Oetiker <tobi\@oetiker.ch>
DOC
credits => <<DOC,
This structure of this probe module is heavily based on
L<Smokeping::probes::CiscoRTTMonEchoICMP|Smokeping::probes::CiscoRTTMonEchoICMP>
by Joerg.Kummer at Roche.com.
DOC
notes => <<DOC,
${e}head2 MENU NAMES
This probe uses the menu name of a test as part of the unique
index. If the menu name is longer than 32 characters, the last
32 characters are used for the index. Collisions are *B<not>*
detected and simply cause one test's results to be used for
all colliding names.
${e}head2 CONFIGURATION
This probe requires read/write access to the pingCtlTable.
It also requires read-only access to the pingResultsTable and the
pingHistoryTable. The DISMAN-PING-MIB is structured such that
it is possible to restrict by pingCtlOwnerIndex. This probe
uses a pingCtlOwnerIndex of "SP on hostname"
as ownerindex by default; use B<ownerindex> to configure this if needed.
${e}head2 SAMPLE JUNOS CONFIGURATION
This configuration permits the community "pinger" read-write
access to the full DISMAN-PING-MIB, but only when sourced
from the manager at B<192.0.2.134>.
snmp {
view pingMIB {
oid .1.3.6.1.2.1.80 include;
}
community pinger {
view pingMIB;
authorization read-write;
clients {
192.0.2.134/32;
}
}
}
${e}head2 SAMPLE CONFIGURATIONS NOTE
This configuration allows the "pinger" community full access to the
DISMAN-PING-MIB. There is information in the description of
B<pingCtlOwnerIndex> in RFC 4560 (L<http://tools.ietf.org/html/rfc4560>)
about using the vacmViewTreeFamilyTable to further restrict access.
The author has not tried this method.
DOC
#${e}head2 SAMPLE IOS CONFIGURATION
#
#Note: I have no clue if IOS supports DISMAN-PING-MIB.
#
# access-list 2 permit 192.0.2.134
# snmp-server view pingMIB .1.3.6.1.2.1.80 included
# snmp-server community pinger view pingMIB RW 2
#
};
}
sub probevars {
my $class = shift;
# This is structured a little differently than the
# average probe. _makevars prefers values from the
# first argument, but we have to override the superclass's
# pings value. So, we put our values in the first argument.
# However, _makevars modifies its second argument, and we
# don't want to modify the superclass's value, so we
# make a copy in $tmp.
my $tmp = { %{ $class->SUPER::probevars } };
return $class->_makevars(
{
pings => {
_re => '\d+',
_default => 15,
_example => 15,
_sub => sub {
my $val = shift;
return
"ERROR: for DismanPing, pings must be between 1 and 15."
unless $val >= 1 and $val <= 15;
return undef;
},
_doc => <<DOC,
How many pings should be sent to each target. Note that the maximum value
for DismanPing MIP is 15, which is less than the SmokePing default, so this
class has its own default value. If your Database section specifies a
value less than 15, you must also set it for this probe.
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
},
},
$tmp
);
}
sub targetvars {
my $class = shift;
return $class->_makevars(
$class->SUPER::targetvars,
{
_mandatory => ['pinghost'],
ownerindex => {
_doc => <<DOC,
The SNMP OwnerIndex to use when setting up the test.
When using VACM, can map to a Security Name or Group Name
of the entity running the test.
By default this will be set to
DOC
_example => "smokeping"
},
pinghost => {
_example => 'pinger@router.example.com',
_doc => <<DOC,
The (mandatory) pinghost parameter specifies the remote system which will
execute the pings, as well as the SNMP community string on the device.
DOC
},
pingsrc => {
_example => '192.0.2.9',
_doc => <<DOC,
The (optional) pingsrc parameter specifies the source address to be used
for pings. If specified, this parameter must identify an IP address
assigned to pinghost.
DOC
},
# tos => {
# _example => 160,
# _default => 0,
# _doc => <<DOC,
#The (optional) tos parameter specifies the value of the ToS byte in
#the IP header of the pings. Multiply DSCP values times 4 and Precedence
#values times 32 to calculate the ToS values to configure, e.g. ToS 160
#corresponds to a DSCP value 40 and a Precedence value of 5.
#DOC
# },
packetsize => {
_doc => <<DOC,
The packetsize parameter lets you configure the packet size for the pings
sent. The minimum is 8, the maximum 65507. Use the same number as with
fping if you want the same packet sizes being used on the network.
DOC
_default => 56,
_re => '\d+',
_sub => sub {
my $val = shift;
return "ERROR: packetsize must be between 8 and 65507"
unless $val >= 8 and $val <= 65507;
return undef;
}
},
}
);
}
# XXX
# This is copied from basefork.pm; it actually belongs
# in basevars.pm.
sub pod_variables {
my $class = shift;
my $pod = $class->SUPER::pod_variables;
my $targetvars = $class->targetvars;
$pod .= "Supported target-specific variables:\n\n";
$pod .= $class->_pod_variables($targetvars);
return $pod;
}
sub new($$$) {
my $proto = shift;
my $class = ref($proto) || $proto;
my $self = $class->SUPER::new(@_);
# no need for this if we run as a cgi
unless ( $ENV{SERVER_SOFTWARE} ) {
# Initialization stuff that might take time
}
return $self;
}
sub ProbeDesc($) {
my $self = shift;
my $bytes = $self->{properties}{packetsize} || 56;
return "DISMAN ICMP Echo Pings ($bytes Bytes)";
}
# RFC 4560:
# A single SNMP PDU can be used to create and start a remote
# ping test. Within the PDU, pingCtlTargetAddress should be set to the
# target host's address (pingCtlTargetAddressType will default to
# ipv4(1)), pingCtlAdminStatus to enabled(1), and pingCtlRowStatus to
# createAndGo(4).
#
# At least one implementation doesn't implement a default for
# pingCtlTargetAddressType (and the MIB itself doesn't specify
# such a default)
#
# Philosophically, I'd like to just leave the row there and
# re-enable the test if the row is there - but there's no easy
# way to verify that the values haven't changed since the last
# time we set it.
sub ping($) {
my $self = shift;
my $pending = {};
my $longest = 0;
my $start = time;
# Empty out any RTTs from the last round. Otherwise, if we get an
# SNMP error for a target, we'll report his last result.
$self->{rtts} = {};
foreach my $t ( @{ $self->targets } ) {
my $addr = $t->{addr};
my $idx = idx($t);
my $host = host($t);
# Delete any existing row. Ignore error.
#Smokeping::do_log("DismanPing deleting for $host $t->{vars}{menu}");
my $ret =
snmpset( $host, "pingCtlRowStatus.$idx", "integer", 6 ); #destroy
if ( !defined($ret) ) {
my ( $err ) = ( $SNMP_Session::errmsg =~ /error status: (\S+)/ );
my $msgmap = {
'notWritable' => 'does the remote support DISMAN-PING-MIB?',
'inconsistentValue' => 'is an old ping running?',
'noAccess' => 'is access control set up properly?'
};
if ( !defined( $err ) ) {
# errmsg can have arbitrary text on the first line.
$err = "SNMP error";
}
# SNMP::Session already carp()d errmsg, so don't include it here.
# It's already in the log.
Smokeping::do_log( "DismanPing: got $err trying to clean up $t->{vars}{host}" .
( $msgmap->{ $err } ? " -- " . $msgmap->{ $err } : "" ) );
next;
}
my $targetaddr = inet_aton($addr);
if ( not defined $targetaddr ) {
Smokeping::do_log("DismanPing can't resolve destination address $addr for $t->{vars}{host}");
next;
}
#XXX consider ipv6 - esp. what does inet_aton() return
#XXX todo: test failure handling code by setting ProbeCOunt and MaxRows
# differently than pings
my @values = (
"pingCtlTargetAddressType.$idx", "integer", 1, #ipv4
"pingCtlTargetAddress.$idx", "octetstring", $targetaddr,
"pingCtlFrequency.$idx", "gauge", 0, # run test only once
"pingCtlTimeOut.$idx", "gauge", 3, # timeout ping after 3 seconds (this is also the interval for sending pings)
"pingCtlProbeCount.$idx", "gauge", $t->{vars}{pings},
"pingCtlMaxRows.$idx", "gauge", $t->{vars}{pings},
"pingCtlAdminStatus.$idx", "integer", 1, #enabled
"pingCtlRowStatus.$idx", "integer", 4, #createAndGo
);
# add pingsrc, packetsize into @values if defined
if ( defined $t->{vars}{packetsize} ) {
unshift( @values,
"pingCtlDataSize.$idx", "gauge", $t->{vars}{packetsize} );
}
if ( defined $t->{vars}{pingsrc} ) {
my $srcaddr = inet_aton( $t->{vars}{pingsrc} );
if ( not defined $srcaddr ) {
Smokeping::do_log("WARNING: DismanPing can't resolve source address $t->{vars}{pingsrc} for $t->{vars}{host}");
}
else {
unshift(
@values,
"pingCtlSourceAddressType.$idx", "integer", 1, #ipv4
"pingCtlSourceAddress.$idx", "octetstring", $srcaddr
);
}
}
# Todo: pingCtlDSField.
# Todo: pingCtlTimeout.
my @snmpsetret;
if ( ( @snmpsetret = snmpset( $host, @values ) )
and defined $snmpsetret[0] ) {
$pending->{ $t->{tree} } = 1;
}
else {
Smokeping::do_log( "ERROR: DismanPing row creation failed for $t->{vars}{host} on $t->{vars}{pinghost}: $SNMP_Session::errmsg" );
}
my $timeout = 3; # XXX DEFVAL for pingCtlTimeOut
my $length = $t->{vars}{pings} * $timeout;
if ( $length > $longest ) {
$longest = $length;
}
}
my $setup = time - $start;
Smokeping::do_debuglog(
"DismanPing took $setup s to set up, now sleeping for $longest s");
sleep($longest);
my $allok = 0;
my $startend = time;
while ( !$allok ) {
$allok = 1;
foreach my $t ( @{ $self->targets } ) {
next unless ( $pending->{ $t->{tree} } );
my $idx = idx($t);
my $host = host($t);
# check if it's done - pingResultsOperStatus != 1
my $status = snmpget( $host, "pingResultsOperStatus.$idx" );
if ( not defined $status or $status == 1 ) {
# if SNMP fails, assume it's not done.
my $howlong = time - $start;
if ( $howlong > $self->step ) {
Smokeping::do_log( "DismanPing: abandoning $t->{vars}{host} after $howlong seconds" );
$pending->{ $t->{tree} } = 0;
}
else {
Smokeping::do_log( "DismanPing: $t->{vars}{host} is still running after $howlong seconds" );
$allok = 0;
}
next;
}
# if so, get results from History Table
my @times = ();
# TODO: log message if you have a bad status other than TimedOut
my $ret = snmpmaptable(
$host,
sub() {
my ( $index, $rtt, $status ) = @_;
push @times, [ sprintf( "%.10e", $rtt / 1000 ), $status ];
},
"pingProbeHistoryResponse.$idx",
"pingProbeHistoryStatus.$idx"
);
Smokeping::do_debuglog( "DismanPing: table download returned "
. ( defined($ret) ? $ret : "undef" ) );
# Make sure we have exactly pings results.
# Fewer are probably an implementation problem (we asked for
# 15, it said the test was done, but didn't return 15).
# More are a less-bad implementation problem - we can keep
# the last 15.
if ( @times < $t->{vars}{pings} ) {
Smokeping::do_log( "DismanPing: $t->{vars}{host} only returned "
. scalar(@times)
. " results" );
@times = ();
}
elsif ( @times > $t->{vars}{pings} ) {
Smokeping::do_log( "DismanPing: $t->{vars}{host} returned "
. scalar(@times)
. " results, taking last $t->{vars}{pings}" );
@times = @times[ $#times - $t->{vars}{pings} .. $#times ];
}
if (@times) {
my (@goodtimes) = ();
foreach my $result (@times) {
push( @goodtimes, $result->[0] )
if ( $result->[1] == 1 ); # responseReceived(1)
}
$self->{rtts}{ $t->{tree} } = [ sort { $a <=> $b } @goodtimes ];
}
$pending->{ $t->{tree} } = 0;
}
sleep 5 unless ($allok);
}
my $howlong = time - $start;
my $endtime = time - $startend;
Smokeping::do_debuglog( "DismanPing took $howlong total, $endtime collecting results");
}
# Return index string for this test:
# INDEX {
# pingCtlOwnerIndex,
# pingCtlTestName
# }
# This is the full index for pingCtlTable and
# pingResultsTable, and the prefix of the index for
# pingProbeHistoryTable.
#
# Uses the last 32 characters of menu name to
# get a unique test name.
sub idx ($) {
my $t = shift;
my $ownerindex = substr($t->{vars}{ownerindex} || 'SP on '.hostname(),0,32);
print STDERR $ownerindex;
my $testname = substr($t->{vars}{host} . ' ICMP ping',0,32);
return join( ".",
length($ownerindex), unpack( "C*", $ownerindex ),
length($testname), unpack( "C*", $testname )
);
}
sub host ($) {
my $t = shift;
# gotta be aggressive with the SNMP to keep within
# the time budget, so set the timeout to 1 second
# and only try twice.
# hostname:port:timeout:retries:backoff:version
return $t->{vars}{pinghost} . "::1:2::2";
}
1;

View File

@@ -0,0 +1,276 @@
package Smokeping::probes::EchoPing;
=head1 301 Moved Permanently
This is a Smokeping probe module. Please use the command
C<smokeping -man Smokeping::probes::EchoPing>
to view the documentation or the command
C<smokeping -makepod Smokeping::probes::EchoPing>
to generate the POD document.
=cut
use strict;
use base qw(Smokeping::probes::basefork);
use Carp;
my $DEFAULTBIN = "/usr/bin/echoping";
sub pod_hash {
return {
name => <<DOC,
Smokeping::probes::EchoPing - an echoping(1) probe for SmokePing
DOC
overview => <<DOC,
Measures TCP or UDP echo (port 7) roundtrip times for SmokePing. Can also be
used as a base class for other echoping(1) probes.
DOC
description => <<DOC,
See echoping(1) for details of the options below.
DOC
bugs => <<DOC,
Should we test the availability of the service at startup? After that it's
too late to complain.
The location of the echoping binary should probably be a global variable
instead of a probe-specific one. As things are, every EchoPing -derived probe
has to declare it if the default ($DEFAULTBIN) isn't correct.
DOC
authors => <<'DOC',
Niko Tyni <ntyni@iki.fi>
DOC
see_also => <<DOC,
echoping(1), L<Smokeping::probes::EchoPingHttp> etc., L<http://echoping.sourceforge.net/>
DOC
}
}
#
# derived class will mess with this through the 'features' method below
my $featurehash = {
waittime => "-w",
timeout => "-t",
size => "-s",
tos => "-P",
priority => "-p",
fill => "-f",
};
sub features {
my $self = shift;
my $newval = shift;
$featurehash = $newval if defined $newval;
return $featurehash;
}
sub new {
my $proto = shift;
my $class = ref($proto) || $proto;
my $self = $class->SUPER::new(@_);
$self->_init if $self->can('_init');
return $self;
}
sub ProbeDesc($) {
return "TCP or UDP Echo pings using echoping(1)";
}
# This can be overridden to tag the port number to the address
# in derived classes (namely EchoPingHttp)
sub make_host {
my $self = shift;
my $target = shift;
return $target->{addr};
}
# This will be overridden by the EchoPingPlugin-derived probes
sub post_args {
return ();
}
# other than host, count and protocol-specific args come from here
sub make_args {
my $self = shift;
my $target = shift;
my @args;
my %arghash = %{$self->features};
for (keys %arghash) {
my $val = $target->{vars}{$_};
push @args, ($arghash{$_}, $val) if defined $val;
}
push @args, $self->ipversion_arg($target);
push @args, $target->{vars}{extraopts} if exists $target->{vars}{extraopts};
return @args;
}
# this is separated to make it possible to test the service
# at startup, although we don't do it at the moment.
sub count_args {
my $self = shift;
my $count = shift;
$count = $self->pings() unless defined $count;
return ("-n", $count);
}
# This is what derived classes will override
sub proto_args {
my $self = shift;
return $self->udp_arg(@_);
}
# UDP is defined only for echo and discard
sub udp_arg {
my $self = shift;
my $target = shift;
my @args;
my $udp = $target->{vars}{udp};
push @args, "-u" if (defined $udp and $udp ne "no" and $udp ne "0");
return @args;
}
sub ipversion_arg {
my $self = shift;
my $target = shift;
my $vers = $target->{vars}{ipversion};
if (defined $vers and $vers =~ /^([46])$/) {
return ("-" . $1);
} else {
$self->do_log("Invalid `ipversion' value: $vers") if defined $vers;
return ();
}
}
sub make_commandline {
my $self = shift;
my $target = shift;
my $count = shift;
$count |= $self->pings($target);
my @args = $self->make_args($target);
my @post_args = $self->post_args($target);
my $host = $self->make_host($target);
push @args, $self->proto_args($target);
push @args, $self->count_args($count);
return ($self->{properties}{binary}, @args, $host, @post_args);
}
sub pingone {
my $self = shift;
my $t = shift;
my @cmd = $self->make_commandline($t);
my $cmd = join(" ", @cmd);
$self->do_debug("executing cmd $cmd");
my @times;
open(P, "$cmd 2>&1 |") or carp("fork: $!");
my @output;
while (<P>) {
chomp;
push @output, $_;
/^Elapsed time: (\d+\.\d+) seconds/ and push @times, $1;
}
close P;
if ($?) {
my $status = $? >> 8;
my $signal = $? & 127;
my $why = "with status $status";
$why .= " [signal $signal]" if $signal;
# only log warnings on the first ping round
my $function = ($self->rounds_count == 1 ? "do_log" : "do_debug");
$self->$function(qq(WARNING: "$cmd" exited $why - output follows));
$self->$function(qq( $_)) for @output;
}
# carp("Got @times") if $self->debug;
return sort { $a <=> $b } @times;
}
sub probevars {
my $class = shift;
my $h = $class->SUPER::probevars;
delete $h->{timeout};
return $class->_makevars($h, {
binary => {
_doc => "The location of your echoping binary.",
_default => $DEFAULTBIN,
_sub => sub {
my $val = shift;
-x $val or return "ERROR: binary '$val' is not executable";
return undef;
},
},
});
}
sub targetvars {
my $class = shift;
return $class->_makevars($class->SUPER::targetvars, {
timeout => {
_doc => 'The "-t" echoping(1) option.',
_example => 1,
_default => 5,
_re => '(\d*\.)?\d+',
},
waittime => {
_doc => 'The "-w" echoping(1) option.',
_example => 1,
_re => '\d+(?:\.\d+)?',
},
size => {
_doc => 'The "-s" echoping(1) option.',
_example => 510,
_re => '\d+',
},
udp => {
_doc => q{The "-u" echoping(1) option. Values other than '0' and 'no' enable UDP.},
_example => 'no',
},
fill => {
_doc => 'The "-f" echoping(1) option.',
_example => 'A',
_re => '.',
},
priority => {
_doc => 'The "-p" echoping(1) option.',
_example => 6,
_re => '\d+',
},
tos => {
_doc => 'The "-P" echoping(1) option.',
_example => '0xa0',
},
ipversion => {
_doc => <<DOC,
The IP protocol used. Possible values are "4" and "6".
Passed to echoping(1) as the "-4" or "-6" options.
DOC
_example => 4,
_re => '[46]'
},
extraopts => {
_doc => 'Any extra options specified here will be passed unmodified to echoping(1).',
_example => '-some-letter-the-author-did-not-think-of',
},
});
}
1;

View File

@@ -0,0 +1,56 @@
package Smokeping::probes::EchoPingChargen;
=head1 301 Moved Permanently
This is a Smokeping probe module. Please use the command
C<smokeping -man Smokeping::probes::EchoPingChargen>
to view the documentation or the command
C<smokeping -makepod Smokeping::probes::EchoPingChargen>
to generate the POD document.
=cut
use strict;
use base qw(Smokeping::probes::EchoPing);
use Carp;
sub pod_hash {
return {
name => <<DOC,
Smokeping::probes::EchoPingChargen - an echoping(1) probe for SmokePing
DOC
overview => <<DOC,
Measures TCP chargen (port 19) roundtrip times for SmokePing.
DOC
notes => <<DOC,
The I<udp> variable is not supported.
DOC
authors => <<'DOC',
Niko Tyni <ntyni@iki.fi>
DOC
see_also => <<DOC,
L<Smokeping::probes::EchoPing>
DOC
}
}
sub proto_args {
return ("-c");
}
sub ProbeDesc($) {
return "TCP Chargen pings using echoping(1)";
}
sub targetvars {
my $class = shift;
my $h = $class->SUPER::targetvars;
delete $h->{udp};
return $h;
}
1;

View File

@@ -0,0 +1,95 @@
package Smokeping::probes::EchoPingDNS;
=head1 301 Moved Permanently
This is a Smokeping probe module. Please use the command
C<smokeping -man Smokeping::probes::EchoPingDNS>
to view the documentation or the command
C<smokeping -makepod Smokeping::probes::EchoPingDNS>
to generate the POD document.
=cut
sub pod_hash {
return {
name => <<DOC,
Smokeping::probes::EchoPingDNS - an echoping(1) probe for SmokePing
DOC
overview => <<DOC,
Measures DNS roundtrip times for SmokePing with the echoping_dns plugin.
DOC
authors => <<'DOC',
Niko Tyni <ntyni@iki.fi>
DOC
notes => <<'DOC',
The I<fill>, I<size> and I<udp> EchoPing variables are not valid.
Plugins, including echoping_dns, are available starting with echoping version 6.
DOC
see_also => <<DOC,
L<Smokeping::probes::EchoPing>,
L<Smokeping::probes::EchoPingPlugin>
DOC
}
}
use strict;
use base qw(Smokeping::probes::EchoPingPlugin);
use Carp;
sub plugin_args {
my $self = shift;
my $target = shift;
my @args = ("-t", $target->{vars}{dns_type});
my $tcp = $target->{vars}{dns_tcp};
if ($tcp and $tcp ne "no") {
push @args, "--tcp";
}
push @args, $target->{vars}{dns_request};
return @args;
}
sub ProbeDesc($) {
return "DNS pings using the echoping_dns plugin";
}
sub targetvars {
my $class = shift;
my $h = $class->SUPER::targetvars;
delete $h->{udp};
delete $h->{fill};
delete $h->{size};
$h->{_mandatory} = [ grep { $_ ne "plugin" } @{$h->{_mandatory}}];
$h->{plugin}{_default} = 'dns';
$h->{plugin}{_example} = '/path/to/dns.so';
return $class->_makevars($h, {
_mandatory => [ 'dns_request' ],
dns_request => {
_doc => <<DOC,
The DNS request (domain name) to be queried.
DOC
_example => 'example.org',
},
dns_type => {
_doc => <<DOC,
The echoping_dns '-t' option: type of data requested (NS, A, SOA etc.)
DOC
_example => 'AAAA',
_default => 'A',
},
dns_tcp => {
_doc => <<DOC,
The echoping_dns '--tcp' option: use only TCP ('virtual circuit').
Enabled if specified with a value other than 'no' or '0'.
DOC
_example => 'yes',
},
},
);
}
1;

View File

@@ -0,0 +1,50 @@
package Smokeping::probes::EchoPingDiscard;
=head1 301 Moved Permanently
This is a Smokeping probe module. Please use the command
C<smokeping -man Smokeping::probes::EchoPingDiscard>
to view the documentation or the command
C<smokeping -makepod Smokeping::probes::EchoPingDiscard>
to generate the POD document.
=cut
sub pod_hash {
return {
name => <<DOC,
Smokeping::probes::EchoPingDiscard - an echoping(1) probe for SmokePing
DOC
overview => <<DOC,
Measures TCP or UDP discard (port 9) roundtrip times for SmokePing.
DOC
authors => <<'DOC',
Niko Tyni <ntyni@iki.fi>
DOC
see_also => <<DOC,
L<Smokeping::probes::EchoPing>
DOC
}
}
use strict;
use base qw(Smokeping::probes::EchoPing);
use Carp;
sub proto_args {
my $self = shift;
my $target = shift;
my @args = $self->udp_arg;
return ("-d", @args);
}
sub ProbeDesc($) {
return "TCP or UDP Discard pings using echoping(1)";
}
1;

View File

@@ -0,0 +1,146 @@
package Smokeping::probes::EchoPingHttp;
=head1 301 Moved Permanently
This is a Smokeping probe module. Please use the command
C<smokeping -man Smokeping::probes::EchoPingHttp>
to view the documentation or the command
C<smokeping -makepod Smokeping::probes::EchoPingHttp>
to generate the POD document.
=cut
use strict;
use base qw(Smokeping::probes::EchoPing);
use Carp;
sub pod_hash {
return {
name => <<DOC,
Smokeping::probes::EchoPingHttp - an echoping(1) probe for SmokePing
DOC
overview => <<DOC,
Measures HTTP roundtrip times (web servers and caches) for SmokePing.
DOC
notes => <<DOC,
You should consider setting a lower value for the C<pings> variable than the
default 20, as repetitive URL fetching may be quite heavy on the server.
The I<fill>, I<size> and I<udp> EchoPing variables are not valid for EchoPingHttp.
DOC
authors => <<'DOC',
Niko Tyni <ntyni@iki.fi>
DOC
see_also => <<DOC,
L<Smokeping::probes::EchoPing>, L<Smokeping::probes::EchoPingHttps>
DOC
}
}
sub _init {
my $self = shift;
# HTTP doesn't fit with filling or size
my $arghashref = $self->features;
delete $arghashref->{size};
delete $arghashref->{fill};
}
# tag the port number after the hostname
sub make_host {
my $self = shift;
my $target = shift;
my $host = $self->SUPER::make_host($target);
my $port = $target->{vars}{port};
$host .= ":$port" if defined $port;
return $host;
}
sub proto_args {
my $self = shift;
my $target = shift;
my $url = $target->{vars}{url};
my @args = ("-h", $url);
# -A : ignore cache
my $ignore = $target->{vars}{ignore_cache};
$ignore = 1
if (defined $ignore and $ignore ne "no"
and $ignore ne "0");
push @args, "-A" if $ignore;
# -a : force cache to revalidate the data
my $revalidate = $target->{vars}{revalidate_data};
$revalidate= 1 if (defined $revalidate and $revalidate ne "no"
and $revalidate ne "0");
push @args, "-a" if $revalidate;
# -R : accept HTTP redirects
my $accept_redirects = $target->{vars}{accept_redirects};
$accept_redirects= 1 if (defined $accept_redirects
and $accept_redirects ne "no"
and $accept_redirects ne "0");
push @args, "-R" if $accept_redirects;
return @args;
}
sub ProbeDesc($) {
return "HTTP pings using echoping(1)";
}
sub targetvars {
my $class = shift;
my $h = $class->SUPER::targetvars;
delete $h->{udp};
delete $h->{fill};
delete $h->{size};
$h->{timeout}{_default} = 10;
$h->{timeout}{_example} = 20;
return $class->_makevars($h, {
url => {
_doc => <<DOC,
The URL to be requested from the web server or cache. Can be either relative
(/...) for web servers or absolute (http://...) for caches.
DOC
_default => '/',
},
port => {
_doc => 'The TCP port to use.',
_example => 80,
_re => '\d+',
},
ignore_cache => {
_doc => <<DOC,
The echoping(1) "-A" option: force the proxy to ignore the cache.
Enabled if the value is anything other than 'no' or '0'.
DOC
_example => 'yes',
},
revalidate_data => {
_doc => <<DOC,
The echoping(1) "-a" option: force the proxy to revalidate data with original
server. Enabled if the value is anything other than 'no' or '0'.
DOC
_example => 'no',
},
accept_redirects => {
_doc => <<DOC,
The echoping(1) "-R" option: Accept HTTP status codes 3xx (redirections)
as normal responses instead of treating them as errors. Note that this option
is only available starting with Echoping 6.
Enabled if the value is anything other than 'no' or '0'.
DOC
_example => 'yes',
},
});
}
1;

View File

@@ -0,0 +1,66 @@
package Smokeping::probes::EchoPingHttps;
=head1 301 Moved Permanently
This is a Smokeping probe module. Please use the command
C<smokeping -man Smokeping::probes::EchoPingHttps>
to view the documentation or the command
C<smokeping -makepod Smokeping::probes::EchoPingHttps>
to generate the POD document.
=cut
use strict;
use base qw(Smokeping::probes::EchoPingHttp);
use Carp;
sub pod_hash {
return {
name => <<DOC,
Smokeping::probes::EchoPingHttps - an echoping(1) probe for SmokePing
DOC
overview => <<DOC,
Measures HTTPS (HTTP over SSL) roundtrip times (web servers and caches) for
SmokePing.
DOC
description => <<DOC,
As EchoPingHttp(3pm), but SSL-enabled.
DOC
notes => <<DOC,
You should consider setting a lower value for the C<pings> variable than the
default 20, as repetitive URL fetching may be quite heavy on the server.
DOC
authors => <<'DOC',
Niko Tyni <ntyni@iki.fi>
DOC
see_also => <<DOC,
L<Smokeping::probes::EchoPingHttp>
DOC
}
}
sub proto_args {
my $self = shift;
my $target = shift;
my @args = $self->SUPER::proto_args($target);
return ("-C", @args);
}
sub ProbeDesc($) {
return "HTTPS pings using echoping(1)";
}
sub targetvars {
my $class = shift;
my $h = $class->SUPER::targetvars;
$h->{prot}{_example} = 3443;
$h->{prot}{_default} = 443;
return $h;
}
1;

View File

@@ -0,0 +1,79 @@
package Smokeping::probes::EchoPingIcp;
=head1 301 Moved Permanently
This is a Smokeping probe module. Please use the command
C<smokeping -man Smokeping::probes::EchoPingIcp>
to view the documentation or the command
C<smokeping -makepod Smokeping::probes::EchoPingIcp>
to generate the POD document.
=cut
use strict;
use base qw(Smokeping::probes::EchoPing);
use Carp;
sub pod_hash {
return {
name => <<DOC,
Smokeping::probes::EchoPingIcp - an echoping(1) probe for SmokePing
DOC
overview => <<DOC,
Measures ICP (Internet Cache Protocol, spoken by web caches)
roundtrip times for SmokePing.
DOC
notes => <<DOC,
The I<fill>, I<size> and I<udp> EchoPing variables are not valid.
DOC
authors => <<'DOC',
Niko Tyni <ntyni@iki.fi>
DOC
see_also => <<DOC,
L<Smokeping::probes::EchoPing>, L<Smokeping::probes::EchoPingHttp>
DOC
}
}
sub _init {
my $self = shift;
# Icp doesn't fit with filling or size
my $arghashref = $self->features;
delete $arghashref->{size};
delete $arghashref->{fill};
}
sub proto_args {
my $self = shift;
my $target = shift;
my $url = $target->{vars}{url};
my @args = ("-i", $url);
return @args;
}
sub ProbeDesc($) {
return "ICP pings using echoping(1)";
}
sub targetvars {
my $class = shift;
my $h = $class->SUPER::targetvars;
delete $h->{udp};
delete $h->{fill};
delete $h->{size};
return $class->_makevars($h, {
_mandatory => [ 'url' ],
url => {
_doc => "The URL to be requested from the web cache.",
_example => 'http://www.example.org/',
},
});
}
1;

View File

@@ -0,0 +1,99 @@
package Smokeping::probes::EchoPingLDAP;
=head1 301 Moved Permanently
This is a Smokeping probe module. Please use the command
C<smokeping -man Smokeping::probes::EchoPingLDAP>
to view the documentation or the command
C<smokeping -makepod Smokeping::probes::EchoPingLDAP>
to generate the POD document.
=cut
sub pod_hash {
return {
name => <<DOC,
Smokeping::probes::EchoPingLDAP - an echoping(1) probe for SmokePing
DOC
overview => <<DOC,
Measures LDAP roundtrip times for SmokePing with the echoping_ldap plugin.
DOC
authors => <<'DOC',
Niko Tyni <ntyni@iki.fi>
DOC
notes => <<'DOC',
The I<fill>, I<size> and I<udp> EchoPing variables are not valid.
Plugins, including echoping_ldap, are available starting with echoping version 6.
DOC
see_also => <<DOC,
L<Smokeping::probes::EchoPing>,
L<Smokeping::probes::EchoPingPlugin>
DOC
}
}
use strict;
use base qw(Smokeping::probes::EchoPingPlugin);
use Carp;
sub plugin_args {
my $self = shift;
my $target = shift;
my @args;
my $req = $target->{vars}{ldap_request};
push @args, "-r $req" if $req;
my $base = $target->{vars}{ldap_base};
push @args, "-b $base" if $base;
my $scope = $target->{vars}{ldap_scope};
push @args, "-s $scope" if $scope;
return @args;
}
sub ProbeDesc($) {
return "LDAP pings using the echoping_ldap plugin";
}
sub targetvars {
my $class = shift;
my $h = $class->SUPER::targetvars;
delete $h->{udp};
delete $h->{fill};
delete $h->{size};
$h->{_mandatory} = [ grep { $_ ne "plugin" } @{$h->{_mandatory}}];
$h->{plugin}{_default} = 'ldap';
$h->{plugin}{_example} = '/path/to/ldap.so';
return $class->_makevars($h, {
ldap_request => {
_doc => <<DOC,
The echoping_ldap '-r' option:
the request to the LDAP server, in LDAP filter language.
DOC
_example => '(objectclass=*)',
},
ldap_base => {
_doc => <<DOC,
The echoping_ldap '-b' option:
base of the search.
DOC
_example => 'dc=current,dc=bugs,dc=debian,dc=org',
},
ldap_scope => {
_doc => <<DOC,
The echoping_ldap '-s' option:
scope of the search, "sub", "one" or "base".
DOC
_example => 'one',
},
},
);
}
1;

View File

@@ -0,0 +1,108 @@
package Smokeping::probes::EchoPingPlugin;
=head1 301 Moved Permanently
This is a Smokeping probe module. Please use the command
C<smokeping -man Smokeping::probes::EchoPingPlugin>
to view the documentation or the command
C<smokeping -makepod Smokeping::probes::EchoPingPlugin>
to generate the POD document.
=cut
sub pod_hash {
return {
name => <<DOC,
Smokeping::probes::EchoPingPlugin - a basis for using echoping(1) plugins as probes for SmokePing
DOC
overview => <<DOC,
Measures roundtrip times for SmokePing with an echoping(1) plugin. The plugins
currently shipped with echoping are implemented as separate probes based
on this class, but the class can also be used directly.
DOC
authors => <<'DOC',
Niko Tyni <ntyni@iki.fi>
DOC
notes => <<'DOC',
The I<fill>, I<size> and I<udp> EchoPing variables are not valid by default for EchoPingPlugin -derived probes.
Plugins are available starting with echoping version 6.
DOC
see_also => <<DOC,
L<Smokeping::probes::EchoPing>,
L<Smokeping::probes::EchoPingLDAP>,
L<Smokeping::probes::EchoPingDNS>,
L<Smokeping::probes::EchoPingWhois>
DOC
}
}
use strict;
use base qw(Smokeping::probes::EchoPing);
use Carp;
sub _init {
my $self = shift;
# plugins don't generally fit with filling, size or udp.
my $arghashref = $self->features;
delete $arghashref->{size};
delete $arghashref->{fill};
delete $arghashref->{udp};
}
sub post_args {
my $self = shift;
my $target = shift;
return $self->plugin_args($target);
}
# derived classes should override this
sub plugin_args {
my $self = shift;
my $target = shift;
return ();
}
sub proto_args {
my $self = shift;
my $target = shift;
my $plugin = $target->{vars}{plugin};
return ("-m", $plugin);
}
sub ProbeDesc($) {
return "Pings using an echoping(1) plugin";
}
sub targetvars {
my $class = shift;
my $h = $class->SUPER::targetvars;
delete $h->{udp};
delete $h->{fill};
delete $h->{size};
return $class->_makevars($h, {
_mandatory => [ 'plugin' ],
plugin => {
_doc => <<DOC,
The echoping plugin that will be used. See echoping(1) for details.
This can either be the name of the plugin or a full path to the
plugin shared object.
DOC
_example => "random",
},
pluginargs => {
_doc => <<DOC,
Any extra arguments needed by the echoping plugin specified with the
I<pluginname> variable. These are generally provided by the subclass probe.
DOC
_example => "-p plugin_specific_arg",
},
},
);
}
1;

View File

@@ -0,0 +1,66 @@
package Smokeping::probes::EchoPingSmtp;
=head1 301 Moved Permanently
This is a Smokeping probe module. Please use the command
C<smokeping -man Smokeping::probes::EchoPingSmtp>
to view the documentation or the command
C<smokeping -makepod Smokeping::probes::EchoPingSmtp>
to generate the POD document.
=cut
use strict;
use base qw(Smokeping::probes::EchoPing);
use Carp;
sub pod_hash {
return {
name => <<DOC,
Smokeping::probes::EchoPingSmtp - an echoping(1) probe for SmokePing
DOC
overview => <<DOC,
Measures SMTP roundtrip times (mail servers) for SmokePing.
DOC
notes => <<DOC,
The I<fill>, I<size> and I<udp> EchoPing variables are not valid.
DOC
authors => <<'DOC',
Niko Tyni <ntyni@iki.fi>
DOC
see_also => <<DOC,
L<Smokeping::probes::EchoPing>
DOC
}
}
sub _init {
my $self = shift;
# SMTP doesn't fit with filling or size
my $arghashref = $self->features;
delete $arghashref->{size};
delete $arghashref->{fill};
}
sub proto_args {
return ("-S");
}
sub ProbeDesc($) {
return "SMTP pings using echoping(1)";
}
sub targetvars {
my $class = shift;
my $h = $class->SUPER::targetvars;
delete $h->{udp};
delete $h->{fill};
delete $h->{size};
return $h;
}
1;

View File

@@ -0,0 +1,78 @@
package Smokeping::probes::EchoPingWhois;
=head1 301 Moved Permanently
This is a Smokeping probe module. Please use the command
C<smokeping -man Smokeping::probes::EchoPingWhois>
to view the documentation or the command
C<smokeping -makepod Smokeping::probes::EchoPingWhois>
to generate the POD document.
=cut
sub pod_hash {
return {
name => <<DOC,
Smokeping::probes::EchoPingWhois - an echoping(1) probe for SmokePing
DOC
overview => <<DOC,
Measures whois roundtrip times for SmokePing with the echoping_whois plugin.
DOC
authors => <<'DOC',
Niko Tyni <ntyni@iki.fi>
DOC
notes => <<'DOC',
The I<fill>, I<size> and I<udp> EchoPing variables are not valid.
Plugins, including echoping_whois, are available starting with echoping version 6.
DOC
see_also => <<DOC,
L<Smokeping::probes::EchoPing>,
L<Smokeping::probes::EchoPingPlugin>
DOC
}
}
use strict;
use base qw(Smokeping::probes::EchoPingPlugin);
use Carp;
sub plugin_args {
my $self = shift;
my $target = shift;
my @args;
push @args, $target->{vars}{whois_request};
return @args;
}
sub ProbeDesc($) {
return "whois pings using the echoping_whois plugin";
}
sub targetvars {
my $class = shift;
my $h = $class->SUPER::targetvars;
delete $h->{udp};
delete $h->{fill};
delete $h->{size};
$h->{_mandatory} = [ grep { $_ ne "plugin" } @{$h->{_mandatory}}];
$h->{plugin}{_default} = 'whois';
$h->{plugin}{_example} = '/path/to/whois.so';
return $class->_makevars($h, {
_mandatory => [ 'whois_request' ],
whois_request => {
_doc => <<DOC,
The request to the whois server (typically a domain name).
DOC
_example => 'example.org',
},
},
);
}
1;

View File

@@ -0,0 +1,286 @@
package Smokeping::probes::FPing;
=head1 301 Moved Permanently
This is a Smokeping probe module. Please use the command
C<smokeping -man Smokeping::probes::FPing>
to view the documentation or the command
C<smokeping -makepod Smokeping::probes::FPing>
to generate the POD document.
=cut
use strict;
use base qw(Smokeping::probes::base);
use IPC::Open3;
use Symbol;
use Carp;
sub pod_hash {
return {
name => <<DOC,
Smokeping::probes::FPing - FPing Probe for SmokePing
DOC
description => <<DOC,
Integrates FPing as a probe into smokeping. The variable B<binary> must
point to your copy of the FPing program. If it is not installed on
your system yet, you can get a slightly enhanced version from L<www.smokeping.org/pub>.
The (optional) B<packetsize> option lets you configure the packetsize for the pings sent.
Since version 3.3 fping sends its statistics to stdout. Set B<usestdout> to 'true'
so make smokeping read stdout instead of stderr.
In B<blazemode>, FPing sends one more ping than requested, and discards
the first RTT value returned as it's likely to be an outlier.
The FPing manpage has the following to say on this topic:
Number of bytes of ping data to send. The minimum size (normally 12) allows
room for the data that fping needs to do its work (sequence number,
timestamp). The reported received data size includes the IP header
(normally 20 bytes) and ICMP header (8 bytes), so the minimum total size is
40 bytes. Default is 56, as in ping. Maximum is the theoretical maximum IP
datagram size (64K), though most systems limit this to a smaller,
system-dependent number.
DOC
authors => <<'DOC',
Tobias Oetiker <tobi@oetiker.ch>
DOC
}
}
sub new($$$)
{
my $proto = shift;
my $class = ref($proto) || $proto;
my $self = $class->SUPER::new(@_);
# no need for this if we run as a cgi
unless ( $ENV{SERVER_SOFTWARE} ) {
my $binary = join(" ", $self->binary);
my $testhost = $self->testhost;
my $return = `$binary -C 1 $testhost 2>&1`;
$self->{enable}{S} = (`$binary -h 2>&1` =~ /\s-S[,\s]/);
$self->{enable}{O} = (`$binary -h 2>&1` =~ /\s-O[,\s]/);
croak "ERROR: fping ('$binary -C 1 $testhost') could not be run: $return"
if $return =~ m/not found/;
croak "ERROR: FPing must be installed setuid root or it will not work\n"
if $return =~ m/only.+root/;
if ($return =~ m/bytes, ([0-9.]+)\sms\s+.*\n.*\n.*:\s+([0-9.]+)/ and $1 > 0){
$self->{pingfactor} = 1000 * $2/$1;
if ($1 != $2){
warn "### fping seems to report in ", $2/$1, " milliseconds (old version?)";
}
} else {
$self->{pingfactor} = 1000; # Gives us a good-guess default
warn "### assuming you are using an fping copy reporting in milliseconds\n";
}
# fping only has -4 and -6 switches starting with 3.16 and the binary refuses
# to run if the switches are passed in to older versions.
$self->{enable}{proto} = (`$binary -v 2>&1` =~ /Version (3.1[6-9]|[4-9])/);
};
return $self;
}
sub ProbeDesc($){
my $self = shift;
my $bytes = $self->{properties}{packetsize}||56;
return "ICMP Echo Pings ($bytes Bytes)";
}
# derived class (ie. RemoteFPing) can override this
sub binary {
my $self = shift;
return $self->{properties}{binary};
}
# derived class (ie. FPing6) can override this
sub testhost {
return "localhost";
}
sub ping ($){
my $self = shift;
# do NOT call superclass ... the ping method MUST be overridden
# increment the internal 'rounds' counter
$self->increment_rounds_count;
my %upd;
my $inh = gensym;
my $outh = gensym;
my $errh = gensym;
# pinging nothing is pointless
return unless @{$self->addresses};
my @params = () ;
push @params, "-$self->{properties}{protocol}" if $self->{properties}{protocol} and $self->{enable}{proto};
push @params, "-b$self->{properties}{packetsize}" if $self->{properties}{packetsize};
push @params, "-t" . int(1000 * $self->{properties}{timeout}) if $self->{properties}{timeout};
push @params, "-i" . int(1000 * $self->{properties}{mininterval});
push @params, "-p" . int(1000 * $self->{properties}{hostinterval}) if $self->{properties}{hostinterval};
push @params, "--iface=$self->{properties}{interface}" if $self->{properties}{interface};
if ($self->rounds_count == 1 and $self->{properties}{sourceaddress} and not $self->{enable}{S}){
$self->do_log("WARNING: your fping binary doesn't support source address setting (-S), I will ignore any sourceaddress configurations - see http://bugs.debian.org/198486.");
}
push @params, "-S$self->{properties}{sourceaddress}" if $self->{properties}{sourceaddress} and $self->{enable}{S};
if ($self->rounds_count == 1 and $self->{properties}{tos} and not $self->{enable}{O}){
$self->do_log("WARNING: your fping binary doesn't support type of service setting (-O), I will ignore any tos configurations.");
}
push @params, "-O$self->{properties}{tos}" if $self->{properties}{tos} and $self->{enable}{O};
my $pings = $self->pings;
if (($self->{properties}{blazemode} || '') eq 'true'){
$pings++;
}
my @cmd = (
$self->binary,
'-C', $pings, '-q','-B1','-r1',
@params,
@{$self->addresses});
$self->do_debug("Executing @cmd");
my $pid = open3($inh,$outh,$errh, @cmd);
$self->{rtts}={};
my $fh = ( $self->{properties}{usestdout} || '') eq 'true' ? $outh : $errh;
while (<$fh>){
chomp;
$self->do_debug("Got fping output: '$_'");
next unless /^\S+\s+:\s+[-\d\.]/; #filter out error messages from fping
my @times = split /\s+/;
my $ip = shift @times;
next unless ':' eq shift @times; #drop the colon
if (($self->{properties}{blazemode} || '') eq 'true'){
shift @times;
}
@times = map {sprintf "%.10e", $_ / $self->{pingfactor}} sort {$a <=> $b} grep /^\d/, @times;
map { $self->{rtts}{$_} = [@times] } @{$self->{addrlookup}{$ip}} ;
}
waitpid $pid,0;
# Exit status (of fping) is
# 0 if all the hosts are reachable,
# 1 if some hosts were unreachable,
# 2 if any IP addresses were not found,
# 3 for invalid command line arguments, and
# 4 for a system call failure.
# Don't log 0 or 1 as an unreachable host is not unexpected for a monitoring software
my $rc = $?;
my $status = $rc >> 8;
carp join(" ",@cmd) . " returned with exit code $rc. run with debug enabled to get more information" unless $status == 0 or $status == 1;
close $inh;
close $outh;
close $errh;
}
sub probevars {
my $class = shift;
return $class->_makevars($class->SUPER::probevars, {
_mandatory => [ 'binary' ],
binary => {
_sub => sub {
my ($val) = @_;
return undef if $ENV{SERVER_SOFTWARE}; # don't check for fping presence in cgi mode
return "ERROR: FPing 'binary' does not point to an executable"
unless -f $val and -x _;
return undef;
},
_doc => "The location of your fping binary.",
_example => '/usr/bin/fping',
},
packetsize => {
_re => '\d+',
_example => 5000,
_sub => sub {
my ($val) = @_;
return "ERROR: FPing packetsize must be between 12 and 64000"
if ( $val < 12 or $val > 64000 );
return undef;
},
_doc => "The ping packet size (in the range of 12-64000 bytes).",
},
blazemode => {
_re => '(true|false)',
_example => 'true',
_doc => "Send an extra ping and then discard the first answer since the first is bound to be an outlier.",
},
protocol => {
_re => '(4|6)',
_example => '4',
_doc => "Choose if the ping should use IPv4 or IPv6.",
},
usestdout => {
_re => '(true|false)',
_example => 'true',
_doc => "Listen for FPing output on stdout instead of stderr ... (version 3.3+ sends its statistics on stdout).",
},
timeout => {
_re => '(\d*\.)?\d+',
_example => 1.5,
_doc => <<DOC,
The fping "-t" parameter, but in (possibly fractional) seconds rather than
milliseconds, for consistency with other Smokeping probes. Note that as
Smokeping uses the fping 'counting' mode (-C), this apparently only affects
the last ping.
DOC
},
hostinterval => {
_re => '(\d*\.)?\d+',
_example => 1.5,
_doc => <<DOC,
The fping "-p" parameter, but in (possibly fractional) seconds rather than
milliseconds, for consistency with other Smokeping probes. From fping(1):
This parameter sets the time that fping waits between successive packets
to an individual target.
DOC
},
mininterval => {
_re => '(\d*\.)?\d+',
_example => .001,
_default => .01,
_doc => <<DOC,
The fping "-i" parameter, but in (probably fractional) seconds rather than
milliseconds, for consistency with other Smokeping probes. From fping(1):
The minimum amount of time between sending a ping packet to any target.
DOC
},
sourceaddress => {
_re => '\d+(\.\d+){3}',
_example => '192.168.0.1',
_doc => <<DOC,
The fping "-S" parameter . From fping(1):
Set source address.
DOC
},
tos => {
_re => '\d+|0x[0-9a-zA-Z]+',
_example => '0x20',
_doc => <<DOC,
Set the type of service (TOS) of outgoing ICMP packets.
You need at laeast fping-2.4b2_to3-ipv6 for this to work. Find
a copy on www.smokeping.org/pub.
DOC
},
interface => {
_example => 'eth0',
_doc => "The name of the network interface to perform the ping on.",
},
});
}
1;

View File

@@ -0,0 +1,62 @@
package Smokeping::probes::FPing6;
=head1 301 Moved Permanently
This is a Smokeping probe module. Please use the command
C<smokeping -man Smokeping::probes::FPing6>
to view the documentation or the command
C<smokeping -makepod Smokeping::probes::FPing6>
to generate the POD document.
=cut
use strict;
use base qw(Smokeping::probes::FPing);
sub pod_hash {
return {
name => <<DOC,
Smokeping::probes::FPing6 - FPing6 Probe for SmokePing
DOC
description => <<DOC,
Integrates FPing6 as a probe into smokeping. This probe is derived from
FPing; the only difference is that the target host used for checking
the fping command output is ::1 instead of localhost.
DOC
authors => <<'DOC',
Tobias Oetiker <tobi@oetiker.ch>
Niko Tyni <ntyni@iki.fi>
DOC
see_also => <<DOC
L<Smokeping::probes::FPing>
DOC
}
}
sub testhost {
return "::1";
}
sub probevars {
my $self = shift;
my $h = $self->SUPER::probevars;
$h->{binary}{_example} = "/usr/bin/fping6";
$h->{protocol}{_example} = "6";
$h->{protocol}{_default} = "6";
$h->{sourceaddress}{_re} = "[0-9A-Fa-f:.]+";
$h->{sourceaddress}{_example} = "::1";
return $h;
}
sub ProbeDesc($){
my $self = shift;
my $bytes = $self->{properties}{packetsize}||56;
return "IPv6-ICMP Echo Pings ($bytes Bytes)";
}
1;

View File

@@ -0,0 +1,447 @@
package Smokeping::probes::FPingContinuous;
=head1 301 Moved Permanently
This is a Smokeping probe module. Please use the command
C<smokeping -man Smokeping::probes::FPingContinuous>
to view the documentation or the command
C<smokeping -makepod Smokeping::probes::FPingContinuous>
to generate the POD document.
=cut
use strict;
use base qw(Smokeping::probes::base);
use IPC::Open3;
use IO::Pipe;
use IO::Select;
use Symbol;
use Carp;
sub pod_hash {
return {
name => <<DOC,
Smokeping::probes::FPingContinuous - FPingContinuous Probe for SmokePing
DOC
description => <<DOC,
Integrates FPingContinuous as a probe into smokeping. The variable B<binary> must
point to your copy of the FPing program. If it is not installed on
your system yet, you can get a slightly enhanced version from L<www.smokeping.org/pub>.
The (optional) B<packetsize> option lets you configure the packetsize for the pings sent.
Continuous output is normally sent to stdout, but you can set B<usestdout> to 'false'
to make smokeping read stderr instead of stdout.
The FPing manpage has the following to say on this topic:
Number of bytes of ping data to send. The minimum size (normally 12) allows
room for the data that fping needs to do its work (sequence number,
timestamp). The reported received data size includes the IP header
(normally 20 bytes) and ICMP header (8 bytes), so the minimum total size is
40 bytes. Default is 56, as in ping. Maximum is the theoretical maximum IP
datagram size (64K), though most systems limit this to a smaller,
system-dependent number.
DOC
authors => <<'DOC',
Steven Wilton <swilton@fluentit.com.au>
Tobias Oetiker <tobi@oetiker.ch>
DOC
}
}
my $pinger_request=undef;
my $pinger_reply=undef;
# Do 5% more pings than required to make sure we have enough results for each poll
my $error_pct=5;
sub new($$$)
{
my $proto = shift;
my $class = ref($proto) || $proto;
my $self = $class->SUPER::new(@_);
# no need for this if we run as a cgi
unless ( $ENV{SERVER_SOFTWARE} ) {
my $binary = join(" ", $self->binary);
my $testhost = $self->testhost;
my $return = `$binary -C 1 $testhost 2>&1`;
$self->{enable}{S} = (`$binary -h 2>&1` =~ /\s-S[,\s]/);
$self->{enable}{O} = (`$binary -h 2>&1` =~ /\s-O[,\s]/);
croak "ERROR: fping ('$binary -C 1 $testhost') could not be run: $return"
if $return =~ m/not found/;
croak "ERROR: FPing must be installed setuid root or it will not work\n"
if $return =~ m/only.+root/;
croak "ERROR: We can only do one ping every 21ms. Either reduce the number of pings or increase the step to fix the issue\n"
if($self->interval() < 20);
if ($return =~ m/bytes, ([0-9.]+)\sms\s+.*\n.*\n.*:\s+([0-9.]+)/ and $1 > 0){
$self->{pingfactor} = 1000 * $2/$1;
if ($1 != $2){
warn "### fping seems to report in ", $2/$1, " milliseconds (old version?)";
}
} else {
$self->{pingfactor} = 1000; # Gives us a good-guess default
warn "### assuming you are using an fping copy reporting in milliseconds\n";
}
};
return $self;
}
sub interval {
my $self=shift;
return (($self->step/$self->pings) * (1-($error_pct/100)) * 1000);
}
sub ProbeDesc($){
my $self = shift;
my $bytes = $self->{properties}{packetsize}||56;
return "ICMP Echo Pings ($bytes Bytes)";
}
# derived class (ie. RemoteFPingContinuous) can override this
sub binary {
my $self = shift;
return $self->{properties}{binary};
}
# derived class (ie. FPingContinuous6) can override this
sub testhost {
return "localhost";
}
sub run_pinger {
my $self=shift;
my $input=shift;
my $output=shift;
my $select = IO::Select->new();
$select->add($input);
my ($fping_stdin, $fping_stdout, $fping_stderr, $fping_pid)=$self->run_fping($select);
my %results=();
foreach my $address(@{$self->addresses}) {
$results{$address}{results}=[];
$results{$address}{assumed_drops}=0;
$results{$address}{reply_seq}=0;
}
while(1) {
my @ready=$select->can_read(1);
foreach my $fh(@ready) {
if($fh->fileno == $input->fileno) {
if($fh->eof) {
$self->do_log("Input pipe has been closed - exiting");
exit(0);
}
my $input_cmd=<$input>;
#$self->do_log($input_cmd);
if($input_cmd =~ /^FETCH (.+)$/) {
my $address=$1;
chomp($address);
if(!exists($results{$address})) {
$self->do_log("We are not gathering results for $address");
print $output "\n";
} else {
my @ret;
if(scalar(@{$results{$address}{results}}) < $self->pings) {
my $fakeloss=$self->pings-scalar(@{$results{$address}{results}});
$self->do_log("Adding $fakeloss lost pings to $address due to insufficient data");
@ret=@{$results{$address}{results}};
# Record the number of assumed drops, adding the error margin to ensure we do not over-report packet loss
$results{$address}{assumed_drops}+=($fakeloss / (1-($error_pct/100)));
while($fakeloss-- > 0) {
push @ret,"-";
}
# Reset the results array
$results{$address}{results}=[];
} else {
# Return the correct number of items from the beginning of the result array
@ret=splice(@{$results{$address}{results}}, 0, $self->pings);
# Leave 2* the error percent of items in the array, but remove extra items
my $extra=scalar(@{$results{$address}{results}}) - ($self->pings * ($error_pct*2/100));
if($extra > 0) {
$self->do_debug("Removing $extra of ". scalar(@{$results{$address}{results}}) ." ping results from array for $address");
splice(@{$results{$address}{results}}, 0, $extra);
} else {
$self->do_debug(scalar(@{$results{$address}{results}}) ." ping results remaining for $address ($extra)");
}
}
$self->do_debug("Data for $address: ". join(" ", @ret));
print $output join(" ", @ret) ."\n";
}
}
} else {
if($fh->eof) {
$self->do_log("fping process exited - restarting");
waitpid $fping_pid,0;
my $rc = $?;
carp "fping process returned with exit code $rc. run with debug enabled to get more information" unless $rc == 0;
close($fping_stdin);
close($fping_stdout);
close($fping_stderr);
$select->remove($fping_stdout);
$select->remove($fping_stderr);
%results=();
foreach my $address(@{$self->addresses}) {
$results{$address}{results}=[];
$results{$address}{assumed_drops}=0;
$results{$address}{reply_seq}=0;
}
($fping_stdin, $fping_stdout, $fping_stderr, $fping_pid)=$self->run_fping($select);
}
while(my $data=<$fh>) {
if($data =~ /(\S+)\s+:\s+\[(\d+)\],.+bytes,\s+([0-9\.]+)\s+ms\s+\(/) {
my $address=$1;
my $this_seq=$2;
my $pingtime=$3;
# See if we missed any sequence numbers since the last reply.
# Also reduce the detected drop count by any assumed loss so we do not over-report packet loss
my $drops=($results{$address}{reply_seq} && $this_seq > $results{$address}{reply_seq})?($this_seq - $results{$address}{reply_seq} - 1 - $results{$address}{assumed_drops}):0;
# Add records for dropped packets
if($drops) {
$self->do_debug("Detected $drops packets dropped in sequence numbers");
while($drops-- > 0) {
push @{$results{$address}{results}}, "-";
}
}
# Record this packet
push @{$results{$address}{results}}, $pingtime;
# Update the sequence number
$results{$address}{reply_seq}=$this_seq;
# We can forget about any assumed drops since we have handles actual packet loss above
$results{$address}{assumed_drops}=0;
} else {
# We only care about input from either stdin or stderr. We need to
# clear data from both to avoid deadlocks, but we only want to log
# garbage data from the fd that is generating the good data
my $data_fh = ( $self->{properties}{usestdout} || '') ne 'false' ? $fping_stdout : $fping_stderr;
if($fh->fileno == $data_fh->fileno) {
$self->do_log("Unknown input data: $data");
}
}
}
}
}
# See if any pipes have been closed
my @gone=$select->has_exception(0);
foreach my $fh(@gone) {
if($fh->fileno == $input->fileno) {
$self->do_log("Input pipe has been closed - exiting");
exit(0);
} else {
$self->do_log("fping process exited - restarting");
waitpid $fping_pid,0;
my $rc = $?;
carp "fping process returned with exit code $rc. run with debug enabled to get more information" unless $rc == 0;
close($fping_stdin);
close($fping_stdout);
close($fping_stderr);
$select->remove($fping_stdout);
$select->remove($fping_stderr);
%results=();
foreach my $address(@{$self->addresses}) {
$results{$address}{results}=[];
$results{$address}{assumed_drops}=0;
$results{$address}{reply_seq}=0;
}
($fping_stdin, $fping_stdout, $fping_stderr, $fping_pid)=$self->run_fping($select);
}
}
}
}
sub run_fping {
my $self = shift;
my $select = shift;
my $inh = gensym;
my $outh = gensym;
my $errh = gensym;
my @params = () ;
push @params, "-b$self->{properties}{packetsize}" if $self->{properties}{packetsize};
push @params, "-t" . int(1000 * $self->{properties}{timeout}) if $self->{properties}{timeout};
push @params, "-p" . int(1000 * $self->{properties}{hostinterval}) if $self->{properties}{hostinterval};
if ($self->rounds_count == 1 and $self->{properties}{sourceaddress} and not $self->{enable}{S}){
$self->do_log("WARNING: your fping binary doesn't support source address setting (-S), I will ignore any sourceaddress configurations - see http://bugs.debian.org/198486.");
}
push @params, "-S$self->{properties}{sourceaddress}" if $self->{properties}{sourceaddress} and $self->{enable}{S};
if ($self->rounds_count == 1 and $self->{properties}{tos} and not $self->{enable}{O}){
$self->do_log("WARNING: your fping binary doesn't support type of service setting (-O), I will ignore any tos configurations.");
}
push @params, "-O$self->{properties}{tos}" if $self->{properties}{tos} and $self->{enable}{O};
my @cmd = (
$self->binary,
'-l','-B1','-r1','-p',$self->interval(),
@params,
@{$self->addresses}
);
$self->do_debug("Executing @cmd");
my $pid = open3($inh,$outh,$errh, @cmd);
$outh->blocking(0);
$errh->blocking(0);
$inh->autoflush(1);
$select->add($outh);
$select->add($errh);
return ($inh,$outh,$errh,$pid);
}
sub ping ($){
my $self = shift;
# do NOT call superclass ... the ping method MUST be overridden
# pinging nothing is pointless
return unless @{$self->addresses};
# Fork off our worker if needed
if(!$pinger_request) {
$pinger_request=IO::Pipe->new();
$pinger_reply=IO::Pipe->new();
my $pid;
if($pid = fork()) { # Parent
$pinger_request->writer();
$pinger_request->autoflush(1);
$pinger_reply->reader();
foreach my $address(@{$self->addresses}) {
map { $self->{rtts}{$_} = undef } @{$self->{addrlookup}{$address}};
}
} elsif(defined($pid)) {
$pinger_request->reader();
$pinger_reply->writer();
$pinger_reply->autoflush(1);
$self->run_pinger($pinger_request, $pinger_reply);
exit(0);
}
} else {
foreach my $address(@{$self->addresses}) {
print $pinger_request "FETCH $address\n";
my $reply=<$pinger_reply>;
chomp($reply);
# Send back the results
my @times = split /\s+/, $reply;
@times = map {sprintf "%.10e", $_ / $self->{pingfactor}} sort {$a <=> $b} grep /^\d/, @times;
map { $self->{rtts}{$_} = [@times] } @{$self->{addrlookup}{$address}};
}
}
}
# If we explicitly set the rtts to undef, we want to record UNDEF for packet loss, which is different from the base module
sub rrdupdate_string($$) {
my $self = shift;
my $tree = shift;
my $pings = $self->pings;
if(exists($self->{rtts}{$tree}) && !defined($self->{rtts}{$tree})) {
$self->do_debug("No data exists - returning undef");
my $age='U';
my $loss='U';
my $median='U';
my @times=map {"U"} 1..($pings);
# Return all values as "U"
return "${age}:${loss}:${median}:".(join ":", @times);
} else {
&Smokeping::probes::base::rrdupdate_string($self, $tree);
}
}
sub probevars {
my $class = shift;
return $class->_makevars($class->SUPER::probevars, {
_mandatory => [ 'binary' ],
binary => {
_sub => sub {
my ($val) = @_;
return undef if $ENV{SERVER_SOFTWARE}; # don't check for fping presence in cgi mode
return "ERROR: FPing 'binary' does not point to an executable"
unless -f $val and -x _;
return undef;
},
_doc => "The location of your fping binary.",
_example => '/usr/bin/fping',
},
packetsize => {
_re => '\d+',
_example => 5000,
_sub => sub {
my ($val) = @_;
return "ERROR: FPing packetsize must be between 12 and 64000"
if ( $val < 12 or $val > 64000 );
return undef;
},
_doc => "The ping packet size (in the range of 12-64000 bytes).",
},
usestdout => {
_re => '(true|false)',
_example => 'false',
_doc => "Listen for FPing output on stdout instead of stderr ... (continuous output is normally sent to stdout).",
},
timeout => {
_re => '(\d*\.)?\d+',
_example => 1.5,
_doc => <<DOC,
The fping "-t" parameter, but in (possibly fractional) seconds rather than
milliseconds, for consistency with other Smokeping probes. Note that as
Smokeping uses the fping 'counting' mode (-C), this apparently only affects
the last ping.
DOC
},
hostinterval => {
_re => '(\d*\.)?\d+',
_example => 1.5,
_doc => <<DOC,
The fping "-p" parameter, but in (possibly fractional) seconds rather than
milliseconds, for consistency with other Smokeping probes. From fping(1):
This parameter sets the time that fping waits between successive packets
to an individual target.
DOC
},
sourceaddress => {
_re => '\d+(\.\d+){3}',
_example => '192.168.0.1',
_doc => <<DOC,
The fping "-S" parameter . From fping(1):
Set source address.
DOC
},
tos => {
_re => '\d+|0x[0-9a-zA-Z]+',
_example => '0x20',
_doc => <<DOC,
Set the type of service (TOS) of outgoing ICMP packets.
You need at laeast fping-2.4b2_to3-ipv6 for this to work. Find
a copy on www.smokeping.org/pub.
DOC
},
});
}
1;

View File

@@ -0,0 +1,240 @@
package Smokeping::probes::FTPtransfer;
=head1 301 Moved Permanently
This is a Smokeping probe module. Please use the command
C<smokeping -man Smokeping::probes::FTPtransfer>
to view the documentation or the command
C<smokeping -makepod Smokeping::probes::FTPtransfer>
to generate the POD document.
=cut
use strict;
use base qw(Smokeping::probes::passwordchecker);
use Net::FTP;
use Time::HiRes qw(gettimeofday sleep);
use Carp;
my $DEFAULTINTERVAL = 1;
sub pod_hash {
return {
name => <<DOC,
Smokeping::probes::FTPtransfer - intrusive bandwidth probe
DOC
overview => <<DOC,
This probe send and retrieve files to or from an ftp server. It will plot
the bandwidth it could use.
DOC
description => <<DOC,
The probe uses the Net::FTP perl client to run performance tests using an
FTP server as a target. This probe is B<intrusive> as it transfers real
data. By using real data we get a fair shot at figuring out what a link is
capable of when it comes to transferring actual files.
The password can be specified either (in order of precedence, with
the latter overriding the former) in the probe-specific variable
`password', in an external file or in the target-specific variable
`password'. The location of this external file is given in the probe-specific
variable `passwordfile'. See Smokeping::probes::passwordchecker(3pm) for the
format of this file (summary: colon-separated triplets of the form
`<host>:<username>:<password>')
The probe tries to be nice to the server and waits at least X seconds
between starting filetransfers, where X is the value of the probe
specific `min_interval' variable ($DEFAULTINTERVAL by default).
Many variables can be specified either in the probe or in the target definition,
the target-specific variable will override the prove-specific variable.
If your transfer takes a lot of time, you may want to make sure to set the
B<timeout> and B<max_rtt> properly so that smokeping does not abort the
transfers of limit the graph size.
DOC
authors => <<'DOC',
Tobias Oetiker <tobi@oetiker.ch> sponsored by Virtela
DOC
bugs => <<DOC,
This probe has the capability for saturating your links, so don't use it
unless you know what you are doing.
The FTPtransfer probe measures bandwidth, but we report the number of
seconds it took to transfer the 'reference' file. This is because currently
the notion of I<Round Trip Time> is at the core of the application. It would
take some re-engineering to split this out in plugins and thus make it
configurable ...
DOC
}
}
# returns the last part of a path
sub _get_filename ($) {
return (split m|/|, $_[0])[-1];
}
sub ProbeDesc ($) {
my $self = shift;
my $srcfile = $self->{properties}{srcfile};
my $destfile = $self->{properties}{destfile} || _get_filename $self->{properties}{srcfile};
my $mode = $self->{properties}{mode};
my $size = $mode eq 'get' ? -s $destfile : -s $srcfile;
return sprintf("FTP File transfers (%.0f KB)",$size/1024);
}
sub new {
my $proto = shift;
my $class = ref($proto) || $proto;
my $self = $class->SUPER::new(@_);
return $self;
}
sub pingone {
my $self = shift;
my $target = shift;
my $host = $target->{addr};
my $vars = $target->{vars};
my $mininterval = $self->{properties}{min_interval};
my $srcfile = $self->{properties}{srcfile};
my $destfile = $self->{properties}{destfile} || _get_filename $self->{properties}{srcfile};
my $mode = $self->{properties}{mode};
my $username = $vars->{username};
$self->do_log("Missing FTP username for $host"), return
unless defined $username;
my $password = $self->password($host, $username) || $vars->{password};
$self->do_log("Missing FTP password for $host/$username"), return
unless defined $password;
my @options = ();
push (@options, Timeout => $vars->{timeout});
push (@options, Port => $vars->{port} ) if $vars->{port};
push (@options, LocalAddr => $vars->{localaddr} ) if $vars->{localaddr};
push (@options, Passive => 1 ) if $vars->{passive} and $vars->{passive} eq 'yes';
my @times;
my $elapsed;
for (1..$self->pings($target)) {
if (defined $elapsed) {
my $timeleft = $mininterval - $elapsed;
sleep $timeleft if $timeleft > 0;
}
my $ftp = Net::FTP->new($host, @options) or
$self->do_log("Problem with $host: ftp session $@"), return;
$ftp->login($username,$password) or
$self->do_log("Problem with $host: ftp login ".$ftp->message), return;
my $start = gettimeofday();
my $ok;
my $size;
if ($mode eq 'get'){
$ok = $ftp->get($srcfile,$destfile) or
$self->do_log("Problem with $host: ftp get ".$ftp->message);
$size = -s $destfile;
} else {
$ok = $ftp->put($srcfile,$destfile) or
$self->do_log("Problem with $host: ftp put ".$ftp->message);
$size = -s $srcfile;
}
my $end = gettimeofday();
$ftp->quit;
$elapsed = ( $end - $start );
$ok or next;
$self->do_debug("$host - $mode mode transferred $size Bytes in ${elapsed}s");
push @times, $elapsed;
}
return sort { $a <=> $b } @times;
}
sub probevars {
my $class = shift;
my $h = $class->SUPER::probevars;
delete $h->{timeout}{_default}; # force a timeout to be defined
$h->{timeout}{_doc} = <<DOC;
The timeout is the maximum amount of time you will allow the probe to
transfer the file. If the probe does not succeed to transfer in the time specified,
it will get killed and a 'loss' will be logged.
Since FTPtransfer is an invasive probe you should make sure you do not load
the link for more than a few seconds anyway. Smokeping currently has a hard
limit of 180 seconds for any RTT.
DOC
return $class->_makevars($h, {
_mandatory => [ 'srcfile','mode','timeout' ],
srcfile => {
_doc => <<DOC,
The name of the source file. If the probe is in B<put> mode, this file
has to be on the local machine, if the probe is in B<get> mode then this
file should sit in the remote ftp account.
DOC
_example => 'src/path/mybig.pdf',
},
destfile => {
_doc => <<DOC,
Normally the destination filename is the same as the source filename
(without the path). If you want keep files in different directories this may not
work, and you have to specify destfile as well.
DOC
_example => 'path/to/destinationfile.xxx',
},
mode => {
_doc => <<DOC,
The ftp probe can be in either put or get mode. If it is in put mode then it will send a file to the ftp server. In get mode it will retrieve a file
from the ftp server.
DOC
_example => 'get',
_re => '(put|get)',
},
min_interval => {
_default => $DEFAULTINTERVAL,
_doc => "The minimum interval between each starting ftp sessions in seconds.",
_re => '(\d*\.)?\d+',
},
});
}
sub targetvars {
my $class = shift;
return $class->_makevars($class->SUPER::targetvars, {
username => {
_doc => 'The username to be tested.',
_example => 'test-user',
},
password => {
_doc => 'The password for the user, if not present in the password file.',
_example => 'test-password',
},
timeout => {
_doc => "Timeout in seconds for the FTP transfer to complete.",
_re => '\d+',
_example => 10,
},
port => {
_doc => 'A non-standard FTP port to be used',
_re => '\d+',
_example => '3255',
},
localaddr => {
_doc => 'The local address to be used when making connections',
_example => 'myhost-nat-if',
},
passive => {
_doc => 'Use passive FTP protocol',
_re => '(yes|no)',
_example => 'yes',
}
});
}
1;

View File

@@ -0,0 +1,264 @@
package Smokeping::probes::IOSPing;
=head1 301 Moved Permanently
This is a Smokeping probe module. Please use the command
C<smokeping -man Smokeping::probes::IOSPing>
to view the documentation or the command
C<smokeping -makepod Smokeping::probes::IOSPing>
to generate the POD document.
=cut
use strict;
use base qw(Smokeping::probes::basefork);
use IPC::Open2;
use Symbol;
use Carp;
my $e = "=";
sub pod_hash {
return {
name => <<DOC,
Smokeping::probes::IOSPing - Cisco IOS Probe for SmokePing
DOC
description => <<DOC,
Integrates Cisco IOS as a probe into smokeping. Uses the rsh / remsh
protocol to run a ping from an IOS device.
DOC
notes => <<DOC,
=head2 IOS Configuration
The IOS device must have rsh enabled and an appropriate trust defined,
eg:
!
ip rcmd rsh-enable
ip rcmd remote-host smoke 192.168.1.2 smoke enable
!
Some IOS devices have a maximum of 5 VTYs available, so be careful not to
hit a limit with the 'forks' variable.
${e}head2 Password authentication
It is not possible to use password authentication with rsh or remsh
due to fundamental limitations of the protocol.
${e}head2 Ping packet size
The FPing manpage has the following to say on the topic of ping packet
size:
Number of bytes of ping data to send. The minimum size (normally 12)
allows room for the data that fping needs to do its work (sequence
number, timestamp). The reported received data size includes the IP
header (normally 20 bytes) and ICMP header (8 bytes), so the minimum
total size is 40 bytes. Default is 56, as in ping. Maximum is the
theoretical maximum IP datagram size (64K), though most systems limit
this to a smaller, system-dependent number.
DOC
authors => <<'DOC',
Paul J Murphy <paul@murph.org>
based on L<Smokeping::probes::FPing|Smokeping::probes::FPing> by
Tobias Oetiker <tobi@oetiker.ch>
DOC
}
}
sub new($$$)
{
my $proto = shift;
my $class = ref($proto) || $proto;
my $self = $class->SUPER::new(@_);
# no need for this if we run as a cgi
unless ( $ENV{SERVER_SOFTWARE} ) {
$self->{pingfactor} = 1000; # Gives us a good-guess default
print "### assuming you are using an IOS reporting in milliseconds\n";
};
return $self;
}
sub ProbeDesc($){
my $self = shift;
my $bytes = $self->{properties}{packetsize};
return "Cisco IOS - ICMP Echo Pings ($bytes Bytes)";
}
sub pingone ($$){
my $self = shift;
my $target = shift;
my $bytes = $self->{properties}{packetsize};
# do NOT call superclass ... the ping method MUST be overridden
my %upd;
my $inh = gensym;
my $outh = gensym;
my @args = ();
my $pings = $self->pings($target);
push(@args,$self->{properties}{binary});
push(@args,'-l',$target->{vars}{iosuser})
if defined $target->{vars}{iosuser};
push(@args,$target->{vars}{ioshost});
push(@args,'ping');
my $pid = open2($outh,$inh,@args);
#
# The following comments are the dialog produced by
# "remsh <router> ping" to a Cisco 800 series running IOS 12.2T
#
# Other hardware or versions of IOS may need adjustments here.
#
# Protocol [ip]:
print { $inh } "\n";
# Target IP address:
print { $inh } $target->{addr},"\n";
# Repeat count [5]:
print { $inh } $pings,"\n";
# Datagram size [100]:
print { $inh } $bytes,"\n";
# Timeout in seconds [2]:
print { $inh } "\n";
# Extended commands [n]:
print { $inh } "y\n";
# Source address or interface:
print { $inh } "".($target->{vars}{iosint} || "") ,"\n";
# Added by Mars Wei to make
# Source address an option
# Type of service [0]:
print { $inh } "\n";
# Set DF bit in IP header? [no]:
print { $inh } "\n";
# Validate reply data? [no]:
print { $inh } "\n";
# Data pattern [0xABCD]:
print { $inh } "\n";
# Loose, Strict, Record, Timestamp, Verbose[none]:
print { $inh } "V\n";
# Loose, Strict, Record, Timestamp, Verbose[V]:
print { $inh } "\n";
# Sweep range of sizes [n]:
print { $inh } "\n";
#
# Type escape sequence to abort.
# Sending 20, 56-byte ICMP Echos to 192.168.1.2, timeout is 2 seconds:
# Reply to request 0 (4 ms)
# Reply to request 1 (4 ms)
# Reply to request 2 (4 ms)
# Reply to request 3 (1 ms)
# Reply to request 4 (1 ms)
# Reply to request 5 (1 ms)
# Reply to request 6 (4 ms)
# Reply to request 7 (4 ms)
# Reply to request 8 (4 ms)
# Reply to request 9 (4 ms)
# Reply to request 10 (1 ms)
# Reply to request 11 (1 ms)
# Reply to request 12 (1 ms)
# Reply to request 13 (1 ms)
# Reply to request 14 (4 ms)
# Reply to request 15 (4 ms)
# Reply to request 16 (4 ms)
# Reply to request 17 (4 ms)
# Reply to request 18 (1 ms)
# Reply to request 19 (1 ms)
# Success rate is 100 percent (20/20), round-trip min/avg/max = 1/2/4 ms
my @times = ();
while (<$outh>){
chomp;
/^Reply to request \d+ \((\d+) ms\)/ && push(@times,$1);
}
@times = map {sprintf "%.10e", $_ / $self->{pingfactor}} sort {$a <=> $b} @times;
waitpid $pid,0;
my $rc = $?;
carp join(" ",@args) . " returned with exit code $rc. run with debug enabled to get more information" unless $rc == 0;
close $inh;
close $outh;
return @times;
}
sub probevars {
my $class = shift;
return $class->_makevars($class->SUPER::probevars, {
_mandatory => ['binary'],
binary => {
_doc => <<DOC,
The binary option specifies the path of the binary to be used to
connect to the IOS device. Commonly used binaries are /usr/bin/rsh
and /usr/bin/remsh, although any script or binary should work if can
be called as
/path/to/binary [ -l user ] router ping
to produce the IOS ping dialog on stdin & stdout.
DOC
_example => '/usr/bin/rsh',
_sub => sub {
my $val = shift;
-x $val or return "ERROR: binary '$val' is not executable";
return undef;
},
},
packetsize => {
_doc => <<DOC,
The (optional) packetsize option lets you configure the packetsize for
the pings sent.
DOC
_default => 56,
_re => '\d+',
_sub => sub {
my $val = shift;
return "ERROR: packetsize must be between 12 and 64000"
unless $val >= 12 and $val <= 64000;
return undef;
},
},
});
}
sub targetvars {
my $class = shift;
return $class->_makevars($class->SUPER::targetvars, {
_mandatory => [ 'ioshost' ],
ioshost => {
_doc => <<DOC,
The ioshost option specifies the IOS device which should be used for
the ping.
DOC
_example => 'my.cisco.router',
},
iosuser => {
_doc => <<DOC,
The (optional) iosuser option allows you to specify the remote
username the IOS device. If this option is omitted, the username
defaults to the default user used by the remsh command (usually the
user running the remsh command, ie the user running SmokePing).
DOC
_example => 'admin',
},
iosint => {
_doc => <<DOC,
The (optional) iosint option allows you to specify the source address
or interface in the IOS device. The value should be an IP address or
an interface name such as "Ethernet 1/0". If this option is omitted,
the IOS device will pick the IP address of the outbound interface to
use.
DOC
_example => 'Ethernet 1/0',
},
});
}
1;

View File

@@ -0,0 +1,512 @@
package Smokeping::probes::IRTT;
=head1 301 Moved Permanently
This is a Smokeping probe module. Please use the command
C<smokeping -man Smokeping::probes::IRTT>
to view the documentation or the command
C<smokeping -makepod Smokeping::probes::IRTT>
to generate the POD document.
=cut
use strict;
use base qw(Smokeping::probes::basefork);
#use Data::Dumper;
use IPC::Open2 qw(open2);
use JSON::PP qw(decode_json);
use Path::Tiny qw(path);
use Scalar::Util qw(looks_like_number);
use Symbol qw(gensym);
use Time::HiRes qw(usleep gettimeofday tv_interval);
sub pod_hash {
return {
name => <<DOC,
Smokeping::probes::IRTT - a SmokePing Probe for L<IRTT|https://github.com/peteheist/irtt>
DOC
description => <<DOC,
This SmokePing probe uses L<IRTT|https://github.com/peteheist/irtt> to record
network L<round-trip time|https://en.wikipedia.org/wiki/Round-trip_delay_time>,
L<one-way delay|https://en.wikipedia.org/wiki/End-to-end_delay> or
L<IPDV|https://en.wikipedia.org/wiki/Packet_delay_variation> (jitter), based on
the value of the B<metric> variable.
Additionally, the probe provides a results sharing feature, which allows using
results from a single IRTT run to record multiple metrics for a given host at
the same time. One target is defined with the B<writeto> variable set, which
selects the name of a temporary file to save the IRTT output to. Additional
targets are defined with the B<readfrom> variable set to the same value, which,
instead of running IRTT, wait for the main target's output to become available,
then parse it to record the chosen metric from the same data. See the
B<writeto> and B<readfrom> variables for more information.
=head2 WARNING
The results sharing feature (B<writeto> and B<readfrom> variables) requires the
number of B<forks> for the IRTT probe to be at least the total number of IRTT
targets defined (regardless of whether they have B<writeto> and B<readfrom>
set). Otherwise, there can be a deadlock while B<readfrom> targets wait for their
corresponding B<writeto> target to complete, which may never start.
DOC
authors => <<'DOC',
Pete Heist <pete@heistp.net>
DOC
};
}
sub new ($$$) {
my $self = shift->SUPER::new(@_);
# no need for this if we run as a cgi (still run at startup)
unless ( $ENV{SERVER_SOFTWARE} ) {
# check irtt version
my $vout = `$self->{properties}->{binary} version`
or die "ERROR: irtt version return code " . ($? >> 8);
if ($vout =~ /irtt version: (\d+)\.(\d+)\.(\d+)/ ) {
if ($1 == '0' && $2 < '9') {
die "ERROR: unsupported irtt version: $1.$2.$3";
}
} else {
die "ERROR: irtt version unexpected output: $vout";
}
};
return $self;
}
sub probevars ($) {
my $class = shift;
my $pv = $class->_makevars($class->SUPER::probevars, {
_mandatory => [ 'binary' ],
binary => {
_doc => "The location of your irtt binary.",
_default => '/usr/bin/irtt',
_example => '/usr/local/bin/irtt',
_sub => sub {
my $val = shift;
return "ERROR: irtt 'binary' does not point to an executable"
unless -f $val and -x _;
return undef;
},
},
tmpdir => {
_doc => "A temporary directory in which to place files for writeto/readfrom.",
_default => '/tmp/smokeping-irtt',
},
});
# TODO Delete probe timeout and define it per-target based on interval
# (not ready yet as need to figure out logic in targetvars)
#delete $pv->{timeout};
return $pv;
}
sub targetvars ($) {
my $class = shift;
my $tv = $class->_makevars($class->SUPER::targetvars, {
dscp => {
_doc => <<DOC,
The packet L<DSCP|https://en.wikipedia.org/wiki/Differentiated_services> value
to use (C<irtt client --dscp>). This is the same as the classic one byte IP ToS
field, but on the modern Internet, typically only the lower 6 bits are used,
and this is called the DSCP value. The upper two bits are reserved for
L<ECN|https://en.wikipedia.org/wiki/Explicit_Congestion_Notification>. Hex may
be used if prefixed by C<0x>.
DOC
_example => '46',
_re => '(\d+|0x[0-9a-fA-F]{1,2})',
},
extraargs => {
_doc => <<DOC,
Extra arguments to C<irtt client> (see L<irtt-client(1)>). B<Be careful> with
extra arguments, as some can corrupt the results.
DOC
_example => '--ttl=32',
},
fill => {
_doc => <<DOC,
The fill to use in the payload for the client to server packet (C<irtt client
--fill>). The B<length> variable must be large enough so there's a payload to fill.
Use rand for random fill, or see L<irtt-client(1)> for more options.
DOC
_example => 'rand',
},
hmac => {
_doc => <<DOC,
The
L<HMAC|https://en.wikipedia.org/wiki/Hash-based_message_authentication_code>
key to use when sending packets to the server (C<irtt client --hmac>).
DOC
_example => 'opensesame',
},
interval => {
_doc => <<DOC,
The interval between successive requests, in seconds (C<irtt client -i>, but the
unit is always seconds (s)).
B<WARNING>
If B<interval> is increased to greater than 5 seconds, the B<timeout> (which
defaults to B<pings> * 5 seconds + 1) must be modified so that SmokePing
doesn't kill the probe prematurely. Additionally, B<interval> must not be
increased such that B<pings> * B<interval> is greater than B<step>. For
example, at B<step>=300 and B<pings>=20, the B<interval> must not be greater
than 15 seconds, but should preferably be less to account for handshake and
packet wait times.
DOC
_example => 1.5,
_default => 1,
_re => '(\d*\.)?\d+',
},
ipversion => {
_doc => <<DOC,
The IP version to use for packets (4 or 6, corresponding to C<irtt client -4>
or C<irtt client -6>). By default the IP version is chosen based on the
supplied host variable.
DOC
_example => 6,
_re => '^(4|6)$',
},
length => {
_doc => <<DOC,
The length (size) of the packet (C<irtt client -l>). The length includes IRTT
headers, but not IP or UDP headers. The actual packet length is increased to
accommodate the IRTT headers, if necessary. Header size as of IRTT 0.9.0 as used
in SmokePing is 48 bytes when B<writeto> is set (since both monotonic and wall
clock values are requested) and 40 bytes otherwise.
DOC
_example => 172,
_re => '\d+',
},
localaddr => {
_doc => <<DOC,
The local address to bind to when sending packets (C<irtt client --local>).
See L<irtt-client(1)> Host formats for valid syntax.
DOC
_example => '192.168.1.10:63814',
},
metric => {
_doc => <<DOC,
The metric to record, one of:
=over
=item *
rtt: L<round-trip time|https://en.wikipedia.org/wiki/Round-trip_delay_time>
=item *
send: L<one-way send delay|https://en.wikipedia.org/wiki/End-to-end_delay>
I<(requires external time synchronization)>
=item *
receive: L<one-way receive delay|https://en.wikipedia.org/wiki/End-to-end_delay>
I<(requires external time synchronization)>
=item *
ipdv: L<IPDV|https://en.wikipedia.org/wiki/Packet_delay_variation>
(instantaneous packet delay variation, or jitter)
=item *
send_ipdv: IPDV for sent packets
=item *
receive_ipdv: IPDV for received packets
=back
Note that the C<send> and C<receive> metrics require accurate external system
clock synchronization, otherwise the values from one will be abnormally high and
the other will be abnormally low or even negative, in which case the value 0
will be given SmokePing. It is recommended to install ntp on both the SmokePing
client and IRTT server. Properly configured NTP may be able to synchronize time to
within a few milliseconds, which is usually enough to provide useful results.
PTP over a LAN may achieve microsecond-level accuracy. For best results between
geographically remote hosts, GPS receivers may be used. Since C<send_ipdv> and
C<receive_ipdv> measure the variation in times between successive packets,
and since C<rtt> and C<ipdv> use monotonic clock values on the client side
only, external time synchronization is not required for these metrics.
DOC
_default => 'rtt',
_re => '^(rtt|send|receive|ipdv|send_ipdv|receive_ipdv)$',
},
readfrom => {
_doc => <<DOC,
The name of a file to read results from, instead of running IRTT. Use in
combination with B<writeto> to use the results from one IRTT run to record
multiple metrics. The value will become the name of a file in B<tmpdir>, and
must be the same as another target's setting for B<writeto>. Multiple targets
may use the same value for B<readfrom>, but B<writeto> and B<readfrom> may not
be both set for a given target. When B<readfrom> is set, any variables that
affect C<irtt client> are ignored because IRTT is not being invoked, including:
B<dscp>, B<extraargs>, B<fill>, B<hmac>, B<interval>, B<ipversion>, B<length>,
B<localaddr> and B<serverfill>. These values are only relevant in the
corresponding B<writeto> target.
Note that the B<host> variable must still be defined for targets that define
B<readfrom>, otherwise the target won't be used.
When using this feature, be sure to have at least as many B<forks> for the
IRTT probe as you have total IRTT targets defined. See the L</DESCRIPTION>
section for more information.
DOC
_example => 'irtt1',
},
readfrompollinterval => {
_doc => <<DOC,
The integer interval in seconds on which to poll for results when B<readfrom>
is set. Lower numbers will allow B<readfrom> to see the results a bit sooner,
at the cost of higher CPU usage. Polling does not begin until the soonest time
at which the IRTT client could have terminated normally.
DOC
_default => 5,
_re => '[1-9]\d*',
_example => '2',
},
serverfill => {
_doc => <<DOC,
The fill to use in the payload for the server to client packet (C<irtt client
--sfill>). The B<length> variable must be large enough to accommodate a
payload. Use C<rand> for random fill, or see L<irtt-client(1)> for more
options.
DOC
_example => 'rand',
},
sleep => {
_doc => <<DOC,
The amount of time to sleep before starting requests or processing results (a
float in seconds). This may be used to avoid CPU spikes caused by invoking
multiple instances of IRTT at the same time.
DOC
_example => '0.5',
_re => '(\d*\.)?\d+',
},
writeto => {
_doc => <<DOC,
The name of a file to write results to after running IRTT. Use in combination
with B<readfrom> to use the results from this IRTT run to record multiple
metrics. The value will become the name of a file in B<tmpdir>, and any targets
with B<readfrom> set to the same value will use this target's results. There
must be only one target with B<writeto> set for a given file, and B<writeto>
and B<readfrom> may not be both set for a given target.
When using this feature, be sure to have at least as many B<forks> for the IRTT
probe as you have total IRTT targets defined. See the L</DESCRIPTION> section
for more information.
DOC
_example => 'irtt1',
},
});
# TODO Here I would like to be able to set the target-specific timeout
# based on the interval and number of pings, but I'm currently unable to
# get the number of pings in this method, before I have a value for target.
#my $pings = $tv->{pings} ? $tv->{pings} : $class->SUPER::pings();
#$tv->{timeout} = $tv->{interval} * $pings + 5;
return $tv;
}
sub ProbeDesc ($) {
my $self = shift;
return "IRTT round-trips";
}
sub get_json_from_file ($$) {
my $self = shift;
my $target = shift;
my $t = $target;
my $tv = $t->{vars};
my $p = $self->{properties};
my $fname = path($p->{tmpdir}, $tv->{readfrom});
# mark start
my $t0 = [gettimeofday];
# sleep, if requested
usleep($tv->{sleep} * 1000000) if $tv->{sleep};
# wait for earliest possible finish, then 5 seconds at a time
sleep $tv->{interval} * $self->pings($t) + 2;
while (1) {
# break when the file is found
last if -f $fname;
# die if step elapsed, which should never happen as we should
# be killed by smokeping's timeout sooner than this
if (tv_interval ($t0, [gettimeofday]) > $self->step) {
die("ERROR: step elapsed and $fname not found");
}
sleep $tv->{readfrompollinterval};
};
# return file contents
return path($fname)->slurp;
}
sub run_irtt ($$) {
my $self = shift;
my $target = shift;
my $t = $target;
my $tv = $t->{vars};
my $p = $self->{properties};
# choose clock for requested metric
my $clock;
if ($tv->{writeto}) {
$clock = 'both';
} else {
$clock = $tv->{metric} =~ /(send|receive)/ ? 'wall' : 'monotonic';
}
# build command
my $count = $self->pings($t);
my $interval = $tv->{interval};
my $duration = $interval * $count;
my @cmd = (
$p->{binary}, 'client',
'-i', $interval . 's',
'-d', $duration . 's',
'-Q',
'--clock=' . $clock,
'--tstamp=midpoint',
'--stats=none',
'-o', '-',
);
push @cmd, ("-l", $tv->{length}) if $tv->{length};
push @cmd, "--hmac=" . $tv->{hmac} if $tv->{hmac};
push @cmd, "--dscp=" . $tv->{dscp} if $tv->{dscp};
push @cmd, "--fill=" . $tv->{fill} if $tv->{fill};
push @cmd, "--sfill=" . $tv->{serverfill} if $tv->{serverfill};
push @cmd, "--local=" . $tv->{localaddr} if $tv->{localaddr};
push @cmd, "-$tv->{ipversion}" if $tv->{ipversion};
push @cmd, $t->{addr};
# sleep, if requested
usleep($tv->{sleep} * 1000000) if $tv->{sleep};
# execute irtt
$self->do_debug("Executing @cmd");
my $inh = gensym;
my $outh = gensym;
my $pid = open2($outh, $inh, @cmd);
my $out = do { local $/; <$outh> };
waitpid $pid,0;
close $inh;
close $outh;
# write json output atomically if writeto set (empty for errors)
if ($tv->{writeto}) {
path($p->{tmpdir}, $tv->{writeto})->spew($out);
}
# die on non-zero status codes
my $status = $? >> 8;
die "ERROR: irtt client return code $status" if $status;
return $out
}
sub nstos ($) {
my $ns = shift;
return $ns / 1000000000.0;
}
sub median {
my @vals = sort {$a <=> $b} @_;
my $len = @vals;
if ($len%2) {
return $vals[int($len/2)];
} else {
return ($vals[int($len/2)-1] + $vals[int($len/2)])/2;
}
}
sub pingone ($$) {
my $self = shift;
my $target = shift;
my $t = $target;
my $tv = $t->{vars};
my $p = $self->{properties};
# if writeto set, create temp directory or remove temp file
if ($tv->{writeto}) {
if ($tv->{readfrom}) {
die("ERROR: writeto and readfrom must not both be set for the same target");
}
my $d = $p->{tmpdir};
if (-d $d) {
path($d, $tv->{writeto})->remove;
} else {
mkdir $d or die("ERROR: unable to create temp dir $d ($!)");
}
}
# get json from irtt, or file if readfrom set
my $json;
if ($tv->{readfrom}) {
$json = get_json_from_file($self, $target);
} else {
$json = run_irtt($self, $target);
}
die("ERROR: json content empty") if $json eq "";
# decode json
my $dec = decode_json($json) or die "ERROR: decode_json failed $!";
# get times for chosen metric from json
my @times;
foreach my $rt ( @{$dec->{'round_trips'}} ) {
if ($rt->{'lost'} eq 'false') {
my $ns;
my $dl = $rt->{'delay'};
my $pv = $rt->{'ipdv'};
for ($tv->{metric}) {
/^(rtt|send|receive)$/ && do {
$ns = $dl->{$tv->{metric}};
if ($ns < 0) {
$ns = 0;
}
next;
};
/^ipdv$/ && do {
$ns = $pv->{'rtt'};
next;
};
/^send_ipdv$/ && do {
$ns = $pv->{'send'};
next;
};
/^receive_ipdv$/ && do {
$ns = $pv->{'receive'};
next;
};
die("ERROR: impossible metric $tv->{metric}")
}
push @times, nstos(abs($ns)) if looks_like_number($ns);
}
}
# push an extra median value for ipdv, which has one fewer values
# than pings, so there isn't a lost packet reported
if ($tv->{metric} =~ /ipdv/ && @times > 0) {
push @times, median(@times);
}
return sort { $a <=> $b } @times;
}
1;

View File

@@ -0,0 +1,281 @@
package Smokeping::probes::LDAP;
=head1 301 Moved Permanently
This is a Smokeping probe module. Please use the command
C<smokeping -man Smokeping::probes::LDAP>
to view the documentation or the command
C<smokeping -makepod Smokeping::probes::LDAP>
to generate the POD document.
=cut
use strict;
use Smokeping::probes::passwordchecker;
use Net::LDAP;
use Time::HiRes qw(gettimeofday sleep);
use base qw(Smokeping::probes::passwordchecker);
# don't bail out if IO::Socket::SSL
# can't be loaded, just warn
# about it when doing starttls
my $havessl = 0;
eval "use IO::Socket::SSL;";
$havessl = 1 unless $@;
my $DEFAULTINTERVAL = 1;
sub pod_hash {
return {
name => <<DOC,
Smokeping::probes::LDAP - a LDAP probe for SmokePing
DOC
overview => <<DOC,
Measures LDAP search latency for SmokePing
DOC
description => <<DOC,
This probe measures LDAP query latency for SmokePing.
The query is specified by the target-specific variable `filter' and,
optionally, by the target-specific variable `base'. The attributes
queried can be specified in the comma-separated list `attrs'.
The TCP port of the LDAP server and the LDAP version to be used can
be specified by the variables `port' and `version'.
The probe can issue the starttls command to convert the connection
into encrypted mode, if so instructed by the `start_tls' variable.
This requires the 'IO::Socket::SSL' perl module to be installed.
The probe can also optionally do an authenticated LDAP bind, if the `binddn'
variable is present. The password to be used can be specified by the
target-specific variable `password' or in an external file.
The location of this file is given in the probe-specific variable
`passwordfile'. See Smokeping::probes::passwordchecker(3pm) for the format
of this file (summary: colon-separated triplets of the form
`<host>:<bind-dn>:<password>')
The probe tries to be nice to the server and does not send authentication
requests more frequently than once every X seconds, where X is the value
of the target-specific "min_interval" variable ($DEFAULTINTERVAL by default).
DOC
authors => <<'DOC',
Niko Tyni <ntyni@iki.fi>
DOC
bugs => <<DOC,
There should be a way of specifying TLS options, such as the certificates
involved etc.
The probe has an ugly way of working around the fact that the
IO::Socket::SSL class complains if start_tls() is done more than once
in the same program. But It Works For Me (tm).
DOC
}
}
sub ProbeDesc {
return "LDAP queries";
}
sub probevars {
my $class = shift;
my $h = $class->SUPER::probevars;
delete $h->{timeout};
return $h;
}
sub targetvars {
my $class = shift;
return $class->_makevars($class->SUPER::targetvars, {
_mandatory => [ 'filter' ],
port => {
_re => '\d+',
_doc => "TCP port of the LDAP server",
_example => 389,
},
scheme => {
_re => '(ldap|ldaps|ldapi)',
_doc => "LDAP scheme to use: ldap, ldaps or ldapi",
_example => 'ldap',
_default => 'ldap',
},
version => {
_re => '\d+',
_doc => "The LDAP version to be used.",
_example => 3,
},
start_tls => {
_doc => "If true, encrypt the connection with the starttls command. Disabled by default.",
_sub => sub {
my $val = shift;
return "ERROR: start_tls defined but IO::Socket::SSL couldn't be loaded"
if $val and not $havessl;
return undef;
},
_example => "1",
},
timeout => {
_doc => "LDAP query timeout in seconds.",
_re => '\d+',
_example => 10,
_default => 5,
},
base => {
_doc => "The base to be used in the LDAP query",
_example => "dc=foo,dc=bar",
},
filter => {
_doc => "The actual search to be made",
_example => "uid=testuser",
},
attrs => {
_doc => "The attributes queried.",
_example => "uid,someotherattr",
},
binddn => {
_doc => "If present, authenticate the LDAP bind with this DN.",
_example => "uid=testuser,dc=foo,dc=bar",
},
password => {
_doc => "The password to be used, if not present in <passwordfile>.",
_example => "mypass",
},
mininterval => {
_default => $DEFAULTINTERVAL,
_doc => "The minimum interval between each query sent, in (possibly fractional) second
s.",
_re => '(\d*\.)?\d+',
},
scope => {
_doc => "The scope of the query. Can be either 'base', 'one' or 'sub'. See the Net::LDAP documentation for details.",
_example => "one",
_re => "(base|one|sub)",
_default => "sub",
},
verify => {
_doc => "The TLS verification level. Can be either 'none', 'optional', 'require'. See the Net::LDAPS documentation for details.",
_example => "optional",
_re => "(none|optional|require)",
_default => "require",
},
});
}
sub new {
my $proto = shift;
my $class = ref($proto) || $proto;
my $self = $class->SUPER::new(@_);
return $self;
}
sub pingone {
my $self = shift;
my $target = shift;
my $host = $target->{addr};
my $vars = $target->{vars};
my $version = $vars->{version} || 3;
my $port = $vars->{port};
my $mininterval = $vars->{mininterval};
my $binddn = $vars->{binddn};
my $scheme = $vars->{scheme};
my $timeout = $vars->{timeout};
my $scope = $vars->{scope};
my $verify = $vars->{verify};
my $password;
if (defined $binddn) {
$password = $self->password($host, $binddn);
if (defined $vars->{password} and
$vars->{password} ne ($self->{properties}{password}||"")) {
$password = $vars->{password};
}
$password ||= $self->{properties}{password};
}
my $start_tls = $vars->{start_tls};
my $filter = $vars->{filter};
my $base = $vars->{base};
my $attrs = $vars->{attrs};
my @attrs = split(/,/, $attrs||"");
my $attrsref = @attrs ? \@attrs : undef;
my @times;
my $start;
for (1..$self->pings($target)) {
if (defined $start) {
my $elapsed = gettimeofday() - $start;
my $timeleft = $mininterval - $elapsed;
sleep $timeleft if $timeleft > 0;
}
local $IO::Socket::SSL::SSL_Context_obj; # ugly but necessary
$start = gettimeofday();
my $ldap = new Net::LDAP($host, scheme => $scheme, port => $port, version => $version, timeout => $timeout, verify => $verify )
or do {
$self->do_log("connection error on $host: $!");
next;
};
my $mesg;
if ($start_tls) {
$mesg = $ldap->start_tls;
$mesg->code and do {
$self->do_log("start_tls error on $host: " . $mesg->error);
$ldap->unbind;
next;
}
}
if (defined $binddn and defined $password) {
$mesg = $ldap->bind($binddn, password => $password);
} else {
if (defined $binddn and not defined $password) {
$self->do_debug("No password specified for $binddn, doing anonymous bind instead");
}
$mesg = $ldap->bind();
}
$mesg->code and do {
$self->do_log("bind error on $host: " . $mesg->error);
$ldap->unbind;
next;
};
$mesg = $ldap->search(base => $base, filter => $filter,
attrs => $attrsref, scope => $scope);
$mesg->code and do {
$self->do_log("filter error on $host: " . $mesg->error);
$ldap->unbind;
next;
};
$ldap->unbind;
my $end = gettimeofday();
my $elapsed = $end - $start;
$self->do_debug("$host: LDAP query $_ took $elapsed seconds");
push @times, $elapsed;
}
return sort { $a <=> $b } @times;
}
1;

View File

@@ -0,0 +1,203 @@
package Smokeping::probes::NFSping;
=head1 301 Moved Permanently
This is a Smokeping probe module. Please use the command
C<smokeping -man Smokeping::probes::NFSping>
to view the documentation or the command
C<smokeping -makepod Smokeping::probes::NFSping>
to generate the POD document.
=cut
use strict;
use base qw(Smokeping::probes::base);
use IPC::Open3;
use Symbol;
use Carp;
sub pod_hash {
return {
name => <<DOC,
Smokeping::probes::NFSping - NFSping Probe for SmokePing
DOC
description => <<DOC,
Integrates NFSping as a probe into smokeping. The variable B<binary> must
point to your copy of the NFSping program.
NFSping can be downloaded from:
L<https://github.com/mprovost/NFSping>
In B<blazemode>, NFSping sends one more ping than requested, and discards
the first RTT value returned as it's likely to be an outlier.
DOC
authors => <<'DOC',
Tobias Oetiker <tobi@oetiker.ch>
Matt Provost <mprovost@termcap.net>
DOC
}
}
sub new($$$)
{
my $proto = shift;
my $class = ref($proto) || $proto;
my $self = $class->SUPER::new(@_);
# no need for this if we run as a cgi
unless ( $ENV{SERVER_SOFTWARE} ) {
my $binary = join(" ", $self->binary);
my $testhost = $self->testhost;
my $return = `$binary -C 1 $testhost 2>&1`;
croak "ERROR: nfsping ('$binary -C 1 $testhost') could not be run: $return"
if $return =~ m/not found/;
if ($return =~ m/([0-9.]+)\sms\s+.*\n.*\n.*:\s+([0-9.]+)/ and $1 > 0){
$self->{pingfactor} = 1000 * $2/$1;
if ($1 != $2){
warn "### nfsping seems to report in ", $2/$1, " milliseconds (old version?)";
}
} else {
$self->{pingfactor} = 1000; # Gives us a good-guess default
warn "### assuming you are using an nfsping copy reporting in milliseconds\n";
}
};
return $self;
}
sub ProbeDesc{
return "NFSping";
}
# derived class (ie. RemoteNFSping) can override this
sub binary {
my $self = shift;
return $self->{properties}{binary};
}
# derived class (ie. NFSping6) can override this
sub testhost {
return "localhost";
}
sub ping ($){
my $self = shift;
# do NOT call superclass ... the ping method MUST be overridden
# increment the internal 'rounds' counter
$self->increment_rounds_count;
my %upd;
my $inh = gensym;
my $outh = gensym;
my $errh = gensym;
# pinging nothing is pointless
return unless @{$self->addresses};
my @params = () ;
push @params, "-t" . int(1000 * $self->{properties}{timeout}) if $self->{properties}{timeout};
push @params, "-i" . int(1000 * $self->{properties}{mininterval});
push @params, "-p" . int(1000 * $self->{properties}{hostinterval}) if $self->{properties}{hostinterval};
if (($self->{properties}{tcp} || '') eq 'true'){
push @params, "-T";
}
my $pings = $self->pings;
if (($self->{properties}{blazemode} || '') eq 'true'){
$pings++;
}
my @cmd = (
$self->binary,
#'-C', $pings, '-q','-B1','-r1',
'-C', $pings, '-q',
@params,
@{$self->addresses});
$self->do_debug("Executing @cmd");
my $pid = open3($inh,$outh,$errh, @cmd);
$self->{rtts}={};
while (<$errh>){
chomp;
$self->do_debug("Got nfsping output: '$_'");
next unless /^\S+\s+:\s+[-\d\.]/; #filter out error messages from nfsping
my @times = split /\s+/;
my $ip = shift @times;
next unless ':' eq shift @times; #drop the colon
if (($self->{properties}{blazemode} || '') eq 'true'){
shift @times;
}
@times = map {sprintf "%.10e", $_ / $self->{pingfactor}} sort {$a <=> $b} grep /^\d/, @times;
map { $self->{rtts}{$_} = [@times] } @{$self->{addrlookup}{$ip}} ;
}
waitpid $pid,0;
my $rc = $?;
carp join(" ",@cmd) . " returned with exit code $rc. run with debug enabled to get more information" unless $rc == 0;
close $inh;
close $outh;
close $errh;
}
sub probevars {
my $class = shift;
return $class->_makevars($class->SUPER::probevars, {
_mandatory => [ 'binary' ],
binary => {
_sub => sub {
my ($val) = @_;
return undef if $ENV{SERVER_SOFTWARE}; # don't check for nfsping presence in cgi mode
return "ERROR: NFSping 'binary' does not point to an executable"
unless -f $val and -x _;
return undef;
},
_doc => "The location of your nfsping binary.",
_example => '/usr/local/bin/nfsping',
},
blazemode => {
_re => '(true|false)',
_example => 'true',
_doc => "Send an extra ping and then discard the first answer since the first is bound to be an outlier.",
},
tcp => {
_re => '(true|false)',
_example => 'true',
_doc => "Use TCP insteadof UDP.",
},
timeout => {
_re => '(\d*\.)?\d+',
_example => 1.5,
_doc => <<DOC,
The nfsping "-t" parameter, but in (possibly fractional) seconds rather than
milliseconds, for consistency with other Smokeping probes.
DOC
},
hostinterval => {
_re => '(\d*\.)?\d+',
_example => 1.5,
_doc => <<DOC,
The nfsping "-p" parameter, but in (possibly fractional) seconds rather than
milliseconds, for consistency with other Smokeping probes. This
parameter sets the time that nfsping waits between successive packets
to an individual target.
DOC
},
mininterval => {
_re => '(\d*\.)?\d+',
_example => .001,
_default => .01,
_doc => <<DOC,
The nfsping "-i" parameter, but in (probably fractional) seconds rather than
milliseconds, for consistency with other Smokeping probes. This is the
interval between pings to successive targets.
DOC
},
});
}
1;

View File

@@ -0,0 +1,206 @@
package Smokeping::probes::OpenSSHEOSPing;
=head1 301 Moved Permanently
This is a Smokeping probe module. Please use the command
C<smokeping -man Smokeping::probes::OpenSSHEOSPing>
to view the documentation or the command
C<smokeping -makepod Smokeping::probes::OpenSSHEOSPing>
to generate the POD document.
=cut
use strict;
use base qw(Smokeping::probes::basefork);
use Net::OpenSSH;
use Carp;
my $e = "=";
sub pod_hash {
return {
name => <<DOC,
Smokeping::probes::OpenSSHEOSPing - Arista EOS SSH Probe for SmokePing
DOC
description => <<DOC,
Connect to Arista EOS via OpenSSH to run ping commands.
This probe uses the "ping" cli of the Arista EOS. You have
the option to specify which interface the ping is sourced from as well.
DOC
notes => <<DOC,
${e}head2 EOS configuration
The EOS device should have a username/password configured, and
the ssh server must not be disabled.
Make sure to connect to the remote host once from the command line as the
user who is running smokeping. On the first connect ssh will ask to add the
new host to its known_hosts file. This will not happen automatically so the
script will fail to login until the ssh key of your EOS box is in the
known_hosts file.
${e}head2 Requirements
This module requires the L<Net::OpenSSH> and L<IO::Pty> perl modules.
DOC
authors => <<'DOC',
Bill Fenner E<lt>fenner@aristanetworks.comE<gt>
based on L<Smokeping::Probes::OpenSSHJunOSPing> by Tobias Oetiker E<lt>tobi@oetiker.chE<gt>,
which itself is
based on L<Smokeping::probes::TelnetJunOSPing> by S H A N E<lt>shanali@yahoo.comE<gt>.
DOC
}
}
sub new($$$)
{
my $proto = shift;
my $class = ref($proto) || $proto;
my $self = $class->SUPER::new(@_);
$self->{pingfactor} = 1000; # Gives us a good-guess default
return $self;
}
sub ProbeDesc($){
my $self = shift;
my $bytes = $self->{properties}{packetsize};
return "Arista EOS - ICMP Echo Pings ($bytes Bytes)";
}
sub pingone ($$){
my $self = shift;
my $target = shift;
my $source = $target->{vars}{source};
my $dest = $target->{vars}{host};
my $psource = $target->{vars}{psource};
my @output = ();
my $login = $target->{vars}{eosuser};
my $password = $target->{vars}{eospass};
my $bytes = $self->{properties}{packetsize};
my $pings = $self->pings($target);
my $unpriv = $target->{vars}{unpriv} || 0;
# do NOT call superclass ... the ping method MUST be overridden
my %upd;
my @args = ();
my $ssh = Net::OpenSSH->new(
$source,
$login ? ( user => $login ) : (),
$password ? ( password => $password ) : (),
timeout => 60,
batch_mode => 1
);
if ($ssh->error) {
$self->do_log( "OpenSSHEOSPing connecting $source: ".$ssh->error );
return ();
};
if ( $unpriv ) {
@output = $ssh->capture("ping $dest");
} else {
if ( $psource ) {
@output = $ssh->capture("ping $dest repeat $pings size $bytes source $psource");
} else {
@output = $ssh->capture("ping $dest repeat $pings size $bytes");
}
}
if ($ssh->error) {
$self->do_log( "OpenSSHEOSPing running commands on $source: ".$ssh->error );
return ();
};
if ($output[ 0 ] !~ /^PING/) {
$self->do_log( "OpenSSHEOSPing got error on $source for $dest: "." / ".join( @output ) );
return ();
}
my @times = ();
for (@output){
chomp;
/^\d+ bytes from .+: icmp_req=\d+ ttl=\d+ time=(\d+\.\d+) ms$/ and push @times,$1;
}
@times = map {sprintf "%.10e", $_ / $self->{pingfactor}} sort {$a <=> $b} @times;
return @times;
}
sub probevars {
my $class = shift;
return $class->_makevars($class->SUPER::probevars, {
packetsize => {
_doc => <<DOC,
The (optional) packetsize option lets you configure the packetsize for
the pings sent.
DOC
_default => 100,
_re => '\d+',
_sub => sub {
my $val = shift;
return "ERROR: packetsize must be between 12 and 64000"
unless $val >= 12 and $val <= 64000;
return undef;
},
},
});
}
sub targetvars {
my $class = shift;
return $class->_makevars($class->SUPER::targetvars, {
_mandatory => [ 'eosuser', 'eospass', 'source' ],
source => {
_doc => <<DOC,
The source option specifies the EOS device that is going to run the ping commands. This
address will be used for the ssh connection.
DOC
_example => "192.168.2.1",
},
psource => {
_doc => <<DOC,
The (optional) psource option specifies an alternate IP address or
Interface from which you wish to source your pings from. Routers
can have many many IP addresses, and interfaces. When you ping from a
router you have the ability to choose which interface and/or which IP
address the ping is sourced from. Specifying an IP/interface does not
necessarily specify the interface from which the ping will leave, but
will specify which address the packet(s) appear to come from. If this
option is left out the EOS Device will source the packet automatically
based on routing and/or metrics. If this doesn't make sense to you
then just leave it out.
DOC
_example => "192.168.2.129",
},
eosuser => {
_doc => <<DOC,
The eosuser option allows you to specify a username that has ping
capability on the EOS Device.
DOC
_example => 'user',
},
eospass => {
_doc => <<DOC,
The eospass option allows you to specify the password for the username
specified with the option eosuser.
DOC
_example => 'password',
},
unpriv => {
_doc => <<DOC,
If the account is unprivileged, specify the 'unpriv' option.
You must also configure "pings = 5", since that is the only
value supported, and values specified for packetsize or
psource are ignored.
DOC
_example => '1',
},
});
}
1;

View File

@@ -0,0 +1,207 @@
package Smokeping::probes::OpenSSHJunOSPing;
=head1 301 Moved Permanently
This is a Smokeping probe module. Please use the command
C<smokeping -man Smokeping::probes::OpenSSHJunOSPing>
to view the documentation or the command
C<smokeping -makepod Smokeping::probes::OpenSSHJunOSPing>
to generate the POD document.
=cut
use strict;
use base qw(Smokeping::probes::basefork);
use Net::OpenSSH;
use Carp;
my $e = "=";
sub pod_hash {
return {
name => <<DOC,
Smokeping::probes::OpenSSHJunOSPing - Juniper SSH JunOS Probe for SmokePing
DOC
description => <<DOC,
Connect to Juniper JunOS via OpenSSH to run ping commands.
This probe uses the "extended ping" of the Juniper JunOS. You have
the option to specify which interface the ping is sourced from as well.
DOC
notes => <<DOC,
${e}head2 JunOS configuration
The JunOS device should have a username/password configured, as well as
the ability to connect to the VTY(s).
Make sure to connect to the remote host once from the command line as the
user who is running smokeping. On the first connect ssh will ask to add the
new host to its known_hosts file. This will not happen automatically so the
script will fail to login until the ssh key of your juniper box is in the
known_hosts file.
Some JunOS devices have a maximum of 5 VTYs available, so be careful not
to hit a limit with the 'forks' variable.
${e}head2 Requirements
This module requires the L<Net::OpenSSH> and L<IO::Pty>.
DOC
authors => <<'DOC',
Tobias Oetiker E<lt>tobi@oetiker.chE<gt>
based on L<Smokeping::probes::TelnetJunOSPing> by S H A N E<lt>shanali@yahoo.comE<gt>.
DOC
}
}
sub new($$$)
{
my $proto = shift;
my $class = ref($proto) || $proto;
my $self = $class->SUPER::new(@_);
# no need for this if we run as a cgi
unless ( $ENV{SERVER_SOFTWARE} ) {
$self->{pingfactor} = 1000; # Gives us a good-guess default
print "### assuming you are using an JunOS reporting in milliseconds\n";
};
return $self;
}
sub ProbeDesc($){
my $self = shift;
my $bytes = $self->{properties}{packetsize};
my $ret = "Juniper JunOS - ICMP Echo Pings ($bytes Bytes";
if (defined (my $tos = $self->{properties}{tos})){
$ret = " tos $tos";
}
return $ret.")";
}
sub pingone ($$){
my $self = shift;
my $target = shift;
my $source = $target->{vars}{source};
my $dest = $target->{vars}{host};
my $psource = $target->{vars}{psource};
my @output = ();
my $login = $target->{vars}{junosuser};
my $password = $target->{vars}{junospass};
my $bytes = $self->{properties}{packetsize};
my $tos = $self->{properties}{tos};
my $pings = $self->pings($target);
# do NOT call superclass ... the ping method MUST be overridden
my %upd;
my @args = ();
my $ssh = Net::OpenSSH->new(
$source,
$login ? ( user => $login ) : (),
$password ? ( password => $password ) : (),
timeout => 60
);
if ($ssh->error) {
warn "OpenSSHJunOSPing connecting $source: ".$ssh->error."\n";
return undef;
};
my $tosadd = '';
if ( defined $tos){
$tosadd = " tos $tos";
}
if ( $psource ) {
@output = $ssh->capture("ping $dest count $pings size $bytes source $psource$tosadd");
} else {
@output = $ssh->capture("ping $dest count $pings size $bytes$tosadd");
}
$ssh->system("quit");
my @times = ();
for (@output){
chomp;
/^\d+ bytes from \S+[:,] icmp_seq=\d+ (?:ttl|hlim)=\d+ time=(\d+\.\d+) ms$/ and push @times,$1;
}
@times = map {sprintf "%.10e", $_ / $self->{pingfactor}} sort {$a <=> $b} @times;
return @times;
}
sub probevars {
my $class = shift;
return $class->_makevars($class->SUPER::probevars, {
packetsize => {
_doc => <<DOC,
The (optional) packetsize option lets you configure the packetsize for
the pings sent.
DOC
_default => 100,
_re => '\d+',
_sub => sub {
my $val = shift;
return "ERROR: packetsize must be between 12 and 64000"
unless $val >= 12 and $val <= 64000;
return undef;
},
},
tos => {
_doc => <<DOC,
The (optional) type of service for the pings sent.
DOC
_sub => sub {
my $val = shift;
return "ERROR: tos must be 0-255"
if $val and not ( $val =~ /^\d+$/ and int($val) <= 255);
return undef;
},
},
});
}
sub targetvars {
my $class = shift;
return $class->_makevars($class->SUPER::targetvars, {
_mandatory => [ 'junosuser', 'junospass', 'source' ],
source => {
_doc => <<DOC,
The source option specifies the JunOS device that is going to run the ping commands. This
address will be used for the ssh connection.
DOC
_example => "192.168.2.1",
},
psource => {
_doc => <<DOC,
The (optional) psource option specifies an alternate IP address or
Interface from which you wish to source your pings from. Routers
can have many many IP addresses, and interfaces. When you ping from a
router you have the ability to choose which interface and/or which IP
address the ping is sourced from. Specifying an IP/interface does not
necessarily specify the interface from which the ping will leave, but
will specify which address the packet(s) appear to come from. If this
option is left out the JunOS Device will source the packet automatically
based on routing and/or metrics. If this doesn't make sense to you
then just leave it out.
DOC
_example => "192.168.2.129",
},
junosuser => {
_doc => <<DOC,
The junosuser option allows you to specify a username that has ping
capability on the JunOS Device.
DOC
_example => 'user',
},
junospass => {
_doc => <<DOC,
The junospass option allows you to specify the password for the username
specified with the option junosuser.
DOC
_example => 'password',
},
});
}
1;

View File

@@ -0,0 +1,203 @@
package Smokeping::probes::Qstat;
=head1 301 Moved Permanently
This is a Smokeping probe module. Please use the command
C<smokeping -man Smokeping::probes::Qstat>
to view the documentation or the command
C<smokeping -makepod Smokeping::probes::Qstat>
to generate the POD document.
=cut
use strict;
use base qw(Smokeping::probes::basefork);
use IPC::Open3;
use Symbol;
use Carp;
use Time::HiRes qw(usleep);
sub pod_hash {
return {
name => <<DOC,
Smokeping::probes::Qstat - Qstat Probe for SmokePing
DOC
description => <<DOC,
Integrates Qstat as a probe into smokeping. The variable B<binary> must
point to your copy of the Qstat program.
Make sure to set your pings to 10, most Quake servers seem to throttle
after 10 rapid pings.
Set the game parameter to one of the valid options to check a different type
DOC
authors => <<'DOC',
Walter Huf <hufman@gmail.com>
DOC
}
}
sub new($$$)
{
my $proto = shift;
my $class = ref($proto) || $proto;
my $self = $class->SUPER::new(@_);
# no need for this if we run as a cgi
unless ( $ENV{SERVER_SOFTWARE} ) {
my $binary = join(" ", $self->binary);
my $return = `$binary 2>&1`;
$self->{enable}{S} = (`$binary 2>&1` =~ /\s-S\s/);
croak "ERROR: Qstat ('$binary') could not be run: $return"
if $return =~ m/not found/;
};
return $self;
}
sub ProbeDesc($){
my $self = shift;
my $game = $self->{properties}{game}||'q3s';
return "Game server $game pings";
}
# derived class can override this
sub binary {
my $self = shift;
return $self->{properties}{binary};
}
sub pingone($$) {
my $self = shift;
my $address = shift;
my @times;
for (my $count = 0; $count < $self->pings($address); $count++) {
push @times, $self->pinghost($address);
}
return @times
}
sub pinghost($$) {
my $self = shift;
my $address = shift;
my $inh = gensym;
my $outh = gensym;
my $errh = gensym;
my $time;
# pinging nothing is pointless
return unless $address;
$address = $address->{addr};
my @params = ();
push @params, "-nocfg";
push @params, "-xml";
push @params, "-timeout", $self->{properties}{timeout} if $self->{properties}{timeout};
push @params, "-srcip", $self->{properties}{sourceaddress} if $self->{properties}{sourceaddress};
push @params, "-srcport", $self->{properties}{sourceport} if $self->{properties}{sourceport};
push @params, "-" . $self->{properties}{game};
if ($self->{properties}{port} && $address !~ /:/) {
push @params, $address . ':' . $self->{properties}{port};
} else {
push @params, $address;
}
my @cmd = (
$self->binary,
@params);
$self->do_debug("Executing @cmd");
my $pid = open3($inh,$outh,$errh, @cmd);
while (<$outh>){
chomp;
$self->do_debug("Got quakestat output: '$_'");
next unless /^\s*<ping>(\d+)<\/ping>\s*$/; #filter out the ping latency line
$time = $1;
}
waitpid $pid,0;
my $rc = $?;
carp join(" ",@cmd) . " returned with exit code $rc. run with debug enabled to get more information" unless $rc == 0;
close $inh;
close $outh;
close $errh;
return $time/1000.0 if defined($time);
return;
}
sub probevars {
my $class = shift;
return $class->_makevars($class->SUPER::probevars, {
_mandatory => [ 'binary' ],
binary => {
_sub => sub {
my ($val) = @_;
return undef if $ENV{SERVER_SOFTWARE}; # don't check for qstat presence in cgi mode
return "ERROR: Qstat 'binary' does not point to an executable"
unless -f $val and -x _;
return undef;
},
_doc => "The location of your quakestat binary.",
_example => '/usr/bin/quakestatba',
},
game => {
_example => "nexuizs",
_default => "q3s",
_doc => <<DOC,
What game type to check, from the -default flag of quakestat
DOC
},
port => {
_re => '\d+',
_example => 27970,
_doc => <<DOC,
The game server port to check. It can also be overridden by adding :port to the host parameter in the Target config.
DOC
},
timeout => {
_re => '\d+',
_example => 1,
_doc => <<DOC,
The quakestat "-timeout" parameter, in seconds.
DOC
},
mininterval => {
_re => '(\d*\.)?\d+',
_example => .1,
_default => .5,
_doc => <<DOC,
The minimum amount of time between sending a ping packet to the target.
DOC
},
sourceaddress => {
_re => '\d+(\.\d+){3}',
_example => '192.168.0.1',
_doc => <<DOC,
The quakestat "-srcip" parameter . From quakestat(1):
Send packets using this IP address
DOC
},
sourceport => {
_re => '\d{1,5}(-\d{1,5})?',
_example => '9923-9943',
_sub => sub {
my ($val) = @_;
my @ports = split('-', $val);
if (scalar @ports == 2 and $ports[0] > $ports[1]) {
return "ERROR: Qstat invalid source port range";
}
return undef;
},
_doc => <<DOC,
The quakestat "-srcport" parameter . From quakestat(1):
Send packets from these network ports
DOC
},
});
}
1;

View File

@@ -0,0 +1,264 @@
package Smokeping::probes::Radius;
=head1 301 Moved Permanently
This is a Smokeping probe module. Please use the command
C<smokeping -man Smokeping::probes::Radius>
to view the documentation or the command
C<smokeping -makepod Smokeping::probes::Radius>
to generate the POD document.
=cut
use strict;
use base qw(Smokeping::probes::passwordchecker);
use Authen::Radius;
use Time::HiRes qw(gettimeofday sleep);
use Carp;
my $DEFAULTINTERVAL = 1;
sub pod_hash {
return {
name => <<DOC,
Smokeping::probes::Radius - a RADIUS authentication probe for SmokePing
DOC
overview => <<DOC,
Measures RADIUS authentication latency for SmokePing
DOC
description => <<DOC,
This probe measures RADIUS (RFC 2865) authentication latency for SmokePing.
The username to be tested is specified in either the probe-specific or the
target-specific variable `username', with the target-specific one overriding
the probe-specific one.
The password can be specified either (in order of precedence, with
the latter overriding the former) in the probe-specific variable
`password', in an external file or in the target-specific variable
`password'. The location of this file is given in the probe-specific
variable `passwordfile'. See Smokeping::probes::passwordchecker(3pm) for the
format of this file (summary: colon-separated triplets of the form
`<host>:<username>:<password>')
The RADIUS protocol requires a shared secret between the server and the client.
This secret can be specified either (in order of precedence, with the latter
overriding the former) in the probe-specific variable `secret', in an external file
or in the target-specific variable `secret'.
This external file is located by the probe-specific variable `secretfile', and it should
contain whitespace-separated pairs of the form `<host> <secret>'. Comments and blank lines
are OK.
If the optional probe-specific variable `nas_ip_address' is specified, its
value is inserted into the authentication requests as the `NAS-IP-Address'
RADIUS attribute.
The probe tries to be nice to the server and does not send authentication
requests more frequently than once every X seconds, where X is the value
of the target-specific "min_interval" variable ($DEFAULTINTERVAL by default).
DOC
authors => <<'DOC',
Niko Tyni <ntyni@iki.fi>
DOC
bugs => <<DOC,
There should be a more general way of specifying RADIUS attributes.
DOC
}
}
sub ProbeDesc {
return "RADIUS queries";
}
sub new {
my $proto = shift;
my $class = ref($proto) || $proto;
my $self = $class->SUPER::new(@_);
# no need for this if we run as a cgi
unless ($ENV{SERVER_SOFTWARE}) {
if (defined $self->{properties}{secretfile}) {
my @stat = stat($self->{properties}{secretfile});
my $mode = $stat[2];
carp("Warning: secret file $self->{properties}{secretfile} is world-readable\n")
if defined $mode and $mode & 04;
open(S, "<$self->{properties}{secretfile}")
or croak("Error opening specified secret file $self->{properties}{secretfile}: $!");
while (<S>) {
chomp;
next unless /\S/;
next if /^\s*#/;
my ($host, $secret) = split;
carp("Line $. in $self->{properties}{secretfile} is invalid"), next
unless defined $host and defined $secret;
$self->secret($host, $secret);
}
close S;
}
}
return $self;
}
sub secret {
my $self = shift;
my $host = shift;
my $newval = shift;
$self->{secret}{$host} = $newval if defined $newval;
return $self->{secret}{$host};
}
sub pingone {
my $self = shift;
my $target = shift;
my $host = $target->{addr};
my $vars = $target->{vars};
my $mininterval = $vars->{mininterval};
my $username = $vars->{username};
my $secret = $self->secret($host);
if (defined $vars->{secret} and
$vars->{secret} ne ($self->{properties}{secret}||"")) {
$secret = $vars->{secret};
}
$secret ||= $self->{properties}{secret};
my $timeout = $vars->{timeout};
my $allowreject = $vars->{allowreject};
$self->do_debug("$host: radius allowreject is $allowreject");
$allowreject=(defined($allowreject)
and $allowreject eq "true");
$self->do_log("Missing RADIUS secret for $host"), return
unless defined $secret;
$self->do_log("Missing RADIUS username for $host"), return
unless defined $username;
my $password = $self->password($host, $username);
if (defined $vars->{password} and
$vars->{password} ne ($self->{properties}{password}||"")) {
$password = $vars->{password};
}
$password ||= $self->{properties}{password};
$self->do_log("Missing RADIUS password for $host/$username"), return
unless defined $password;
my $port = $vars->{port};
$host .= ":$port" if defined $port;
my @times;
my $elapsed;
for (1..$self->pings($target)) {
if (defined $elapsed) {
my $timeleft = $mininterval - $elapsed;
sleep $timeleft if $timeleft > 0;
}
my $r = new Authen::Radius(Host => $host, Secret => $secret, TimeOut => $timeout);
$r->add_attributes(
{ Name => 1, Value => $username, Type => 'string' },
{ Name => 2, Value => $password, Type => 'string' },
);
$r->add_attributes( { Name => 4, Type => 'ipaddr', Value => $vars->{nas_ip_address} })
if exists $vars->{nas_ip_address};
my $c;
my $start = gettimeofday();
$r->send_packet(&ACCESS_REQUEST) and $c = $r->recv_packet;
my $end = gettimeofday();
my $result;
if (defined $c) {
$result = $c;
$result = "OK" if $c == &ACCESS_ACCEPT;
$result = "fail" if $c == &ACCESS_REJECT;
$result = "fail-OK" if $c == &ACCESS_REJECT and $allowreject;
} else {
if (defined $r->get_error) {
$result = "error: " . $r->strerror;
} else {
$result = "no reply";
}
}
$elapsed = $end - $start;
$self->do_debug("$host: radius query $_: $result, $elapsed");
if (defined $c) {
if ( $c == &ACCESS_ACCEPT or
($c == &ACCESS_REJECT and $allowreject) ) {
push @times, $elapsed;
}
}
}
return sort { $a <=> $b } @times;
}
sub probevars {
my $class = shift;
my $h = $class->SUPER::probevars;
delete $h->{timeout};
return $class->_makevars($h, {
secretfile => {
_doc => <<DOC,
A file containing the RADIUS shared secrets for the targets. It should contain
whitespace-separated pairs of the form `<host> <secret>'. Comments and blank lines
are OK.
DOC
_example => '/another/place/secret',
_sub => sub {
my $val = shift;
-r $val or return "ERROR: secret file $val is not readable.";
return undef;
},
},
});
}
sub targetvars {
my $class = shift;
return $class->_makevars($class->SUPER::targetvars, {
_mandatory => [ 'username' ],
username => {
_doc => 'The username to be tested.',
_example => 'test-user',
},
password => {
_doc => 'The password for the user, if not present in the password file.',
_example => 'test-password',
},
secret => {
_doc => 'The RADIUS shared secret for the target, if not present in the secrets file.',
_example => 'test-secret',
},
nas_ip_address => {
_doc => 'The NAS-IP-Address RADIUS attribute for the authentication requests. Not needed everywhere.',
_example => '10.1.2.3',
},
mininterval => {
_default => $DEFAULTINTERVAL,
_doc => "The minimum interval between each authentication request sent, in (possibly fractional) seconds.",
_re => '(\d*\.)?\d+',
},
timeout => {
_default => 5,
_doc => "Timeout in seconds for the RADIUS queries.",
_re => '\d+',
},
port => {
_doc => 'The RADIUS port to be used',
_re => '\d+',
_example => 1645,
},
allowreject => {
_doc => 'Treat "reject" responses as OK',
_re => '(true|false)',
_example => 'true',
},
});
}
1;

View File

@@ -0,0 +1,130 @@
package Smokeping::probes::RemoteFPing;
=head1 301 Moved Permanently
This is a Smokeping probe module. Please use the command
C<smokeping -man Smokeping::probes::RemoteFPing>
to view the documentation or the command
C<smokeping -makepod Smokeping::probes::RemoteFPing>
to generate the POD document.
=cut
sub pod_hash {
return {
name => <<DOC,
Smokeping::probes::RemoteFPing - Remote FPing Probe for SmokePing
DOC
description => <<DOC,
Integrates the remote execution of FPing via ssh/rsh into smokeping.
The variable B<binary> must point to your copy of the ssh/rsh program.
The variable B<rbinary> must point to your copy of the fping program
at the remote end.
DOC
notes => <<'DOC',
It is important to make sure that you can access the remote machine
without a password prompt, otherwise this probe will not work properly.
To test just try something like this:
$ ssh foo@HostA.foobar.com fping HostB.barfoo.com
The next thing you see must be fping's output.
The B<rhost>, B<ruser>, B<rport> and B<rbinary> variables used to be configured
in the Targets section of the first target or its parents They were moved
to the Probes section, because the variables aren't really target-specific
(all the targets are measured with the same parameters). The Targets
sections aren't recognized anymore.
DOC
authors => <<'DOC',
Luis F Balbinot <hades@inf.ufrgs.br>
Niko Tyni <ntyni@iki.fi>
derived from Smokeping::probes::FPing by
Tobias Oetiker <tobi@oetiker.ch>
DOC
bugs => <<DOC
This functionality should be in a generic 'remote execution' module
so that it could be used for the other probes too.
DOC
}
}
use strict;
use base qw(Smokeping::probes::FPing);
sub ProbeDesc($) {
my $self = shift;
my $superdesc = $self->SUPER::ProbeDesc;
return "Remote $superdesc";
}
sub binary {
my $self = shift;
my @ret = ( $self->SUPER::binary );
for my $what (qw(ruser rport rhost rbinary)) {
my $prefix = ($what eq 'ruser' ? "-l" : "");
my $port = ($what eq 'rport' ? "-p" : "");
if (defined $self->{properties}{$what}) {
push @ret, $prefix . $port . $self->{properties}{$what};
}
}
return @ret;
}
sub probevars {
my $class = shift;
my $h = $class->SUPER::probevars;
$h->{rbinary} = $h->{binary};
delete $h->{binary};
delete $h->{rbinary}{_sub}; # we can't check the remote program's -x bit
@{$h->{_mandatory}} = map { $_ ne 'binary' ? $_ : 'rbinary' } @{$h->{_mandatory}};
return $class->_makevars($h, {
_mandatory => [ 'binary', 'rhost' ],
binary => {
_doc => <<DOC,
This variable specifies the path of the remote shell program (usually ssh,
rsh or remsh). Any other script or binary that can be called as
binary [ -l ruser ] [ -p rport ] rhost rbinary
may be used.
DOC
_example => '/usr/bin/ssh',
_sub => sub {
my $val = shift;
-x $val or return "ERROR: binary '$val' is not executable";
return undef;
},
},
rhost => {
_doc => <<DOC,
The B<rhost> option specifies the remote device from where fping will
be launched.
DOC
_example => 'my.pinger.host',
},
ruser => {
_doc => <<DOC,
The (optional) B<ruser> option allows you to specify the remote user,
if different from the one running the smokeping daemon.
DOC
_example => 'foo',
},
rport => {
_doc => <<DOC,
The (optional) B<rport> option allows you to specify the port of the
remote host.
DOC
_example => '22',
},
});
}
1;

View File

@@ -0,0 +1,173 @@
package Smokeping::probes::SSH;
=head1 301 Moved Permanently
This is a Smokeping probe module. Please use the command
C<smokeping -man Smokeping::probes::SSH>
to view the documentation or the command
C<smokeping -makepod Smokeping::probes::SSH>
to generate the POD document.
=cut
use strict;
use base qw(Smokeping::probes::basefork);
use IPC::Open3;
use Symbol;
use Carp;
use Time::HiRes qw(gettimeofday tv_interval);
sub pod_hash {
return {
name => <<DOC,
Smokeping::probes::SSH - Secure Shell Probe for SmokePing
DOC
description => <<DOC,
Integrates ssh-keyscan as a probe into smokeping. The variable B<binary> must
point to your copy of the ssh-keyscan program. If it is not installed on
your system yet, you should install openssh >= 3.8p1
The Probe asks the given host n-times for it's public key, where n is
the amount specified in the config File.
As part of the initialization, the probe asks 127.0.0.1 for it's public key
and tries to parse the output. This is to ensure that the specified ssh-keyscan
binary provides output in the expected formatm before relying on it.Make sure
you have SSH running on the localhost as well, or specify an alternative
init_host target to test against, that is expected to be available during any
smokeping restart.
DOC
authors => <<'DOC',
Christian Recktenwald <smokeping-contact@citecs.de>
DOC
}
}
my $ssh_re=qr/^# \S+ SSH-/i;
sub new($$$)
{
my $proto = shift;
my $class = ref($proto) || $proto;
my $self = $class->SUPER::new(@_);
# no need for this if we run as a cgi
unless ( $ENV{SERVER_SOFTWARE} ) {
my $call = "$self->{properties}{binary} -t dsa,rsa,ecdsa $self->{properties}{init_host}";
my $return = `$call 2>&1`;
if ($return =~ m/$ssh_re/s){
print "### parsing ssh-keyscan output...OK\n";
} else {
croak "ERROR: output of '$call' does not match $ssh_re\n";
}
};
return $self;
}
sub ProbeDesc($){
my $self = shift;
return "SSH requests";
}
sub pingone ($){
my $self = shift;
my $target = shift;
my $inh = gensym;
my $outh = gensym;
my $errh = gensym;
my $host = $target->{addr};
my $query = "$self->{properties}{binary} -t $target->{vars}->{keytype} -p $target->{vars}->{port}";
my @times;
# if ipv4/ipv6 proto was specified in the target, add it unless it is "0"
if ($target->{vars}->{ssh_af} && $target->{vars}->{ssh_af} ne "0") {
$query .= " -$target->{vars}->{ssh_af}";
}
$query .= " $host";
# get the user and system times before and after the test
$self->do_debug("query=$query\n");
for (my $run = 0; $run < $self->pings; $run++) {
my $t0 = [gettimeofday()];
my $pid = open3($inh,$outh,$errh, $query);
# OpenSSH 9.8 compatibility - output is on stdout now
while (<$outh>) {
if (/$ssh_re/i) {
push @times, tv_interval($t0);
last;
}
}
while (<$errh>) {
if (/$ssh_re/i) {
push @times, tv_interval($t0);
last;
}
}
waitpid $pid,0;
my $rc = $?;
carp "$query returned with exit code $rc. run with debug enabled to get more information" unless $rc == 0;
close $errh;
close $inh;
close $outh;
}
@times = map {sprintf "%.10e", $_ } sort {$a <=> $b} @times;
# $self->do_debug("time=@times\n");
return @times;
}
sub probevars {
my $class = shift;
return $class->_makevars($class->SUPER::probevars, {
_mandatory => [ 'binary' ],
binary => {
_doc => "The location of your ssh-keyscan binary.",
_example => '/usr/bin/ssh-keyscan',
_sub => sub {
my $val = shift;
-x $val or return "ERROR: binary '$val' is not executable";
return undef;
},
},
init_host => {
_doc => "Host to use for initialization, defaults to IPv4 localhost of 127.0.0.1",
_example => '127.0.0.1',
_default => '127.0.0.1',
}
})
}
sub targetvars {
my $class = shift;
return $class->_makevars($class->SUPER::targetvars, {
keytype => {
_doc => "Type of key, used in ssh-keyscan -t I<keytype>",
_re => "[ecdr]sa*",
_example => 'dsa',
_default => 'rsa',
},
port => {
_doc => "Port to use when testing the ssh connection -p I<port>",
_re => '\d+',
_example => '5000',
_default => '22',
},
ssh_af => {
_doc => "Address family (IPv4/IPV6) to use when testing the ssh connection, specify 4 or 6. Specify 0 to reset to default system preference, instead of inheriting the value from parent sections.",
_re => '\d+',
_example => '4',
_default => '0',
},
})
}
1;

View File

@@ -0,0 +1,178 @@
package Smokeping::probes::SendEmail;
# Copyright (c) 2012 Florian Coulmier <florian@coulmier.fr>
#
# 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 3 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, see <http://www.gnu.org/licenses/>. 1
#
=head1 301 Moved Permanently
This is a Smokeping probe module. Please use the command
C<smokeping -man Smokeping::probes::skel>
to view the documentation or the command
C<smokeping -makepod Smokeping::probes::skel>
to generate the POD document.
=cut
use strict;
use base qw(Smokeping::probes::basefork);
use Carp;
use Sys::Hostname;
use Time::HiRes;
use Net::SMTP;
sub pod_hash {
return {
name => <<DOC,
Smokeping::probes::SendEmail - a Smokeping probe that measure time needed to send an mail
DOC
description => <<DOC,
This probe actually send a mail to a MX server and measure time it took. You can choose the sender and recipient address as well as the size of the mail.
DOC
authors => <<'DOC',
Florian Coulmier <florian@coulmier.fr>,
DOC
see_also => <<DOC
L<smokeping_extend>
DOC
};
}
sub new($$$)
{
my $proto = shift;
my $class = ref($proto) || $proto;
my $self = $class->SUPER::new(@_);
# no need for this if we run as a cgi
unless ( $ENV{SERVER_SOFTWARE} ) {
# if you have to test the program output
# or something like that, do it here
# and bail out if necessary
};
return $self;
}
# Probe-specific variables declaration
sub probevars {
my $class = shift;
return $class->_makevars($class->SUPER::probevars, {
_mandatory => [ 'from', 'to' ],
from => {
_doc => "Mail from address",
_example => 'test@test.com',
},
to => {
_doc => "Rcpt to address",
_example => 'test@test.com',
},
subject => {
_doc => "Subject of the mail",
_example => "Test Smokeping",
_default => "Test",
},
bodysize => {
_doc => "Size of the mail to send in bytes. If set to 0, a default mail content will be set. Note that mail always contain From, To and Subject headers.",
_example => "1024",
_default => "0",
}
});
}
# Target-specific variables declaration
sub targetvars {
my $class = shift;
return $class->_makevars($class->SUPER::targetvars, {
port => { _doc => "Port of the SMTP server to reach",
_example => 25,
_default => 25,
},
});
}
sub ProbeDesc($){
my $self = shift;
return "Measure time to send a complete email";
}
# this is where the actual stuff happens
sub pingone ($){
my $self = shift;
my $target = shift;
my @times;
# Retrieve probe-specific and target-specific variables
my $count = $self->pings($target);
my $from = $self->{properties}{from};
my $to = $self->{properties}{to};
my $subject = $self->{properties}{subject} || "Smokeping Test";
my $bodysize = $self->{properties}{bodysize} || 0;
my $host = $target->{addr};
my $port = $target->{vars}{port} || 25;
# Get Hostname
my $hostname = hostname();
# Send a mail as many times as requested
for (1..$count) {
# Start counting time
my $start = Time::HiRes::gettimeofday();
# Open the connection and then send the mail
my $smtp = new Net::SMTP("$host:$port", Timeout => 5, Hello => $hostname);
next if (!$smtp);
$smtp->mail($from) || next;
$smtp->to($to, { Notify => ['NEVER'] }) || next;
$smtp->data() || next;
$smtp->datasend("From: <$from>\n");
$smtp->datasend("To: <$to>\n");
$smtp->datasend("Subject: $subject\n");
$smtp->datasend("\n");
# If user specified a bodysize for the probe, send the request number of characters instead of the default content.
if ($bodysize > 0) {
my $nbLines = $bodysize / 80;
for (1..$nbLines) {
$smtp->datasend(sprintf("%s\n", "A" x 79));
}
$smtp->datasend(sprintf("%s\n", "A" x ($bodysize % 80)));
} else {
$smtp->datasend("This is a test email sent by Smokeping to check speed of mx server $host.\n");
$smtp->datasend("If you receive this mail in your mailbox, you are likely to be spammed in just few minutes!\n");
}
$smtp->dataend() || next;
$smtp->quit();
# End measure of time and save it
my $end = Time::HiRes::gettimeofday();
push(@times, $end - $start);
}
return sort {$a <=> $b } @times;
}
# That's all, folks!
1;

View File

@@ -0,0 +1,148 @@
package Smokeping::probes::SipSak;
=head1 301 Moved Permanently
This is a Smokeping probe module. Please use the command
C<smokeping -man Smokeping::probes::SipSak>
to view the documentation or the command
C<smokeping -makepod Smokeping::probes::SipSak>
to generate the POD document.
=cut
use strict;
use base qw(Smokeping::probes::basefork);
use Carp;
use IO::Select;
sub pod_hash {
return {
name => <<DOC,
Smokeping::probes::SipSak - tests sip server
DOC
overview => <<DOC,
This probe sends OPTIONS messages to a sip server testing the latency.
DOC
description => <<DOC,
The probe uses the L<sipsak|http://sipsak.org/> tool to measure sip server latency by sending an OPTIONS message.
The sipsak command supports a large number of additional parameters to fine-tune its operation. Use the
params variable to configure them.
DOC
authors => <<'DOC',
Tobias Oetiker <tobi@oetiker.ch> sponsored by ANI Networks
DOC
}
}
sub ProbeDesc ($) {
my $self = shift;
return sprintf("SIP OPTIONS messages");
}
sub new {
my $proto = shift;
my $class = ref($proto) || $proto;
my $self = $class->SUPER::new(@_);
return $self;
}
sub pingone {
my $self = shift;
my $target = shift;
my $host = $target->{addr};
my $vars = $target->{vars};
my @times;
my $elapsed;
my $pingcount = $self->pings($target);
my $keep = $vars->{keep_second};
$host = $vars->{user}.'@'.$host if $vars->{user};
$host = $host . ':' . $vars->{port} if $vars->{port};
my @extra_opts = ();
@extra_opts = split /\s/, $vars->{params} if $vars->{params};
open (my $sak,'-|',$self->{properties}{binary},'-vv','-A',$pingcount,'-s','sip:'.$host,@extra_opts)
or die("ERROR: $self->{properties}{binary}: $!\n");
my $sel = IO::Select->new();
$sel->add($sak);
if (not $sel->can_read($vars->{sipsak_timeout})){
$self->do_debug("SipSak: timeout for $host");
return '';
}
my $reply = join ("",<$sak>);
close $sak;
my @reply = split /\*\*\sreply/, $reply;
# don't need the stuff before the first replyx
shift @reply;
my $filter = '.*';
$self->do_debug("SipSak: got ".(scalar @reply)." replies, expected $pingcount");
if (scalar @reply > $pingcount){
$filter = $keep eq 'yes' ? 'final received' : 'provisional received';
}
for my $item (@reply){
$self->do_debug("SipSak: looking at '$item'");
if (not $item =~ /$filter/){
$self->do_debug("SipSak: skipping as there was not match for $filter");
next;
}
if ($item =~ /(?:\sand|\sreceived\safter)\s(\d+(?:\.\d+)?)\sms\s/){
$self->do_debug("SipSak: match");
push @times,$1/1000;
}
else {
$self->do_debug("SipSak: no match");
}
}
return sort { $a <=> $b } @times;
}
sub probevars {
my $class = shift;
my $h = $class->SUPER::probevars;
return $class->_makevars($h, {
binary => {
_doc => "The location of your echoping binary.",
_default => '/usr/bin/sipsak',
_sub => sub {
my $val = shift;
-x $val or return "ERROR: binary '$val' is not executable";
return undef;
},
},
});
}
sub targetvars {
my $class = shift;
return $class->_makevars($class->SUPER::targetvars, {
user => {
_doc => "User to use for sip connection.",
_example => 'nobody',
},
port => {
_doc => "usa non-default port for the sip connection.",
_example => 5061,
},
params => {
_doc => "additional sipsak options. The options will get split on space.",
_example => '--numeric --password=mysecret'
},
keep_second => {
_doc => "If OPTIONS is actually implemented by the server, SipSak will receive two responses. If this option is set, the timing from the second, final response will be counter",
_example => 'yes',
_re => 'yes|no'
},
sipsak_timeout => {
_doc => "Timeout for sipsak in seconds (fractional)",
_default => 2,
},
});
}
1;

View File

@@ -0,0 +1,169 @@
package Smokeping::probes::TCPPing;
=head1 301 Moved Permanently
This is a Smokeping probe module. Please use the command
C<smokeping -man Smokeping::probes::FPing>
to view the documentation or the command
C<smokeping -makepod Smokeping::probes::FPing>
to generate the POD document.
=cut
use strict;
use base qw(Smokeping::probes::basefork);
use IPC::Open3;
use Symbol;
use Carp;
sub pod_hash {
return {
name => <<DOC,
Smokeping::probes::TCPPing - TCPPing Probe for SmokePing
DOC
description => <<DOC,
Integrates TCPPing as a probe into smokeping. The variable B<binary> must
point to your copy of the TCPPing program. If it is not installed on
your system yet, you can get it from https://github.com/deajan/tcpping.
The (optional) port option lets you configure the port for the pings sent.
The TCPPing manpage has the following to say on this topic:
The problem is that with the widespread use of firewalls on the modern Internet,
many of the packets that traceroute(8) sends out end up being filtered,
making it impossible to completely trace the path to the destination.
However, in many cases, these firewalls will permit inbound TCP packets to specific
ports that hosts sitting behind the firewall are listening for connections on.
By sending out TCP SYN packets instead of UDP or ICMP ECHO packets,
tcptraceroute is able to bypass the most common firewall filters.
It is worth noting that tcptraceroute never completely establishes a TCP connection
with the destination host. If the host is not listening for incoming connections,
it will respond with an RST indicating that the port is closed. If the host instead
responds with a SYN|ACK, the port is known to be open, and an RST is sent by
the kernel tcptraceroute is running on to tear down the connection without completing
three-way handshake. This is the same half-open scanning technique that nmap(1) uses
when passed the -sS flag.
DOC
authors => <<'DOC',
Norman Rasmussen <norman@rasmussen.co.za>
Patched for Smokeping 2.x compatibility by Anton Chernev <maznio@doom.bg>
DOC
}
}
sub new($$$)
{
my $proto = shift;
my $class = ref($proto) || $proto;
my $self = $class->SUPER::new(@_);
# no need for this if we run as a cgi
unless ( $ENV{SERVER_SOFTWARE} ) {
my $return = `$self->{properties}{binary} -C -x 1 localhost 2>&1`;
if ($return =~ m/bytes, ([0-9.]+)\sms\s+.*\n.*\n.*:\s+([0-9.]+)/ and $1 > 0){
$self->{pingfactor} = 1000 * $2/$1;
print "### tcpping seems to report in ", $1/$2, " milliseconds\n";
} else {
$self->{pingfactor} = 1000; # Gives us a good-guess default
print "### assuming you are using an tcpping copy reporting in milliseconds\n";
}
};
return $self;
}
sub ProbeDesc($){
my $self = shift;
return "TCP Pings";
}
sub probevars {
my $class = shift;
return $class->_makevars($class->SUPER::probevars, {
_mandatory => [ 'binary' ],
binary => {
_doc => "The location of your tcpping script.",
_example => '/usr/bin/tcpping',
_sub => sub {
my $val = shift;
return "ERROR: TCPPing 'binary' does not point to an executable"
unless -f $val and -x _;
my $return = `$val -C -x 1 localhost 2>&1`;
return "ERROR: tcpping must be installed setuid root or it will not work\n"
if $return =~ m/only.+root/;
return undef;
},
},
});
}
sub targetvars {
my $class = shift;
return $class->_makevars($class->SUPER::targetvars, {
port => {
_doc => "The TCP port the probe should measure.",
_example => '80',
_sub => sub {
my $val = shift;
return "ERROR: TCPPing port must be between 0 and 65535"
if $val and ( $val < 0 or $val > 65535 );
return undef;
},
},
});
}
sub pingone ($){
my $self = shift;
my $target = shift;
# do NOT call superclass ... the ping method MUST be overridden
my $inh = gensym;
my $outh = gensym;
my $errh = gensym;
my @times; # Result times
my @port = () ;
push @port, $target->{vars}{port} if $target->{vars}{port};
my @cmd = (
$self->{properties}{binary},
'-C', '-x', $self->pings($target)
);
push @cmd, $target->{addr}, @port;
$self->do_debug("Executing @cmd");
my $pid = open3($inh,$outh,$errh, @cmd);
while (<$outh>){
chomp;
$self->do_debug("Received: $outh");
next unless /^\S+\s+:\s+[\d\.]/; #filter out error messages from tcpping
@times = split /\s+/;
my $ip = shift @times;
next unless ':' eq shift @times; #drop the colon
@times = map {sprintf "%.10e", $_ / $self->{pingfactor}} sort {$a <=> $b} grep /^\d/, @times;
}
waitpid $pid,0;
my $rc = $?;
carp join(" ",@cmd) . " returned with exit code $rc. run with debug enabled to get more information" unless $rc == 0;
close $inh;
close $outh;
close $errh;
return @times;
}
1;

View File

@@ -0,0 +1,256 @@
package Smokeping::probes::TacacsPlus;
=head1 301 Moved Permanently
This is a Smokeping probe module. Please use the command
C<smokeping -man Smokeping::probes::TacacsPlus>
to view the documentation or the command
C<smokeping -makepod Smokeping::probes::TacacsPlus>
to generate the POD document.
=cut
use strict;
use base qw(Smokeping::probes::passwordchecker);
use Authen::TacacsPlus;
use Time::HiRes qw(gettimeofday sleep);
use Carp;
my $DEFAULTINTERVAL = 1;
sub pod_hash {
return {
name => <<DOC,
Smokeping::probes::TacacsPlus - a TacacsPlus authentication probe for SmokePing
DOC
overview => <<DOC,
Measures TacacsPlus authentication latency for SmokePing
DOC
description => <<DOC,
This probe measures TacacsPlus authentication latency for SmokePing.
The username to be tested is specified in either the probe-specific or the
target-specific variable `username', with the target-specific one overriding
the probe-specific one.
The password can be specified either (in order of precedence, with
the latter overriding the former) in the probe-specific variable
`password', in an external file or in the target-specific variable
`password'. The location of this file is given in the probe-specific
variable `passwordfile'. See Smokeping::probes::passwordchecker(3pm) for the
format of this file (summary: colon-separated triplets of the form
`<host>:<username>:<password>')
The TacacsPlus protocol requires a shared secret between the server and the client.
This secret can be specified either (in order of precedence, with the latter
overriding the former) in the probe-specific variable `secret', in an external file
or in the target-specific variable `secret'.
This external file is located by the probe-specific variable `secretfile', and it should
contain whitespace-separated pairs of the form `<host> <secret>'. Comments and blank lines
are OK.
The default TacacsPlus authentication type is ASCII. PAP and CHAP are also available.
See the Authen::TacacsPlus documentation for more information;
The probe tries to be nice to the server and does not send authentication
requests more frequently than once every X seconds, where X is the value
of the target-specific "min_interval" variable ($DEFAULTINTERVAL by default).
DOC
authors => <<'DOC',
Gary Mikula <g2ugzm@hotmail.com>
DOC
bugs => <<DOC,
Not as yet....
DOC
}
}
sub ProbeDesc {
return "TacacsPlus Authentication Attempts";
}
sub new {
my $proto = shift;
my $class = ref($proto) || $proto;
my $self = $class->SUPER::new(@_);
# no need for this if we run as a cgi
unless ($ENV{SERVER_SOFTWARE}) {
if (defined $self->{properties}{secretfile}) {
my @stat = stat($self->{properties}{secretfile});
my $mode = $stat[2];
carp("Warning: secret file $self->{properties}{secretfile} is world-readable\n")
if defined $mode and $mode & 04;
open(S, "<$self->{properties}{secretfile}")
or croak("Error opening specified secret file $self->{properties}{secretfile}: $!");
while (<S>) {
chomp;
next unless /\S/;
next if /^\s*#/;
my ($host, $secret) = split;
carp("Line $. in $self->{properties}{secretfile} is invalid"), next
unless defined $host and defined $secret;
$self->secret($host, $secret);
}
close S;
}
}
return $self;
}
sub secret {
my $self = shift;
my $host = shift;
my $newval = shift;
$self->{secret}{$host} = $newval if defined $newval;
return $self->{secret}{$host};
}
sub pingone {
my $self = shift;
my $target = shift;
my $host = $target->{addr};
my $vars = $target->{vars};
my $mininterval = $vars->{mininterval};
my $username = $vars->{username};
my $authen_type = $vars->{authtype};
my $secret = $self->secret($host);
my @times;
my $elapsed;
my $result;
my $start;
my $authen;
my $end;
if (defined $vars->{secret} and
$vars->{secret} ne ($self->{properties}{secret}||"")) {
$secret = $vars->{secret};
}
$secret ||= $self->{properties}{secret};
my $timeout = $vars->{timeout};
$self->do_log("Missing TacacsPlus secret for $host"), return
unless defined $secret;
$self->do_log("Missing TacacsPlus username for $host"), return
unless defined $username;
my $password = $self->password($host, $username);
if (defined $vars->{password} and
$vars->{password} ne ($self->{properties}{password}||"")) {
$password = $vars->{password};
}
$password ||= $self->{properties}{password};
$self->do_log("Missing TacacsPlus password for $host/$username"), return
unless defined $password;
my $port = $vars->{port};
$host .= ":$port" if defined $port;
for (1..$self->pings($target)) {
if (defined $elapsed) {
my $timeleft = $mininterval - $elapsed;
sleep $timeleft if $timeleft > 0;
}
$host =~ s/:[0-9]+//g;
my $r = new Authen::TacacsPlus(Host => $host, Key => $secret, Port =>$port);
if( $r ) {
$start = gettimeofday();
if( $authen_type eq 'PAP' ){
$authen = &Authen::TacacsPlus::TAC_PLUS_AUTHEN_TYPE_PAP;
}elsif( $authen_type eq 'CHAP'){
$authen = &Authen::TacacsPlus::TAC_PLUS_AUTHEN_TYPE_CHAP;
}else{
$authen = &Authen::TacacsPlus::TAC_PLUS_AUTHEN_TYPE_ASCII;
}
if( $r->authen($username, $password, $authen ) ) {
$end = gettimeofday();
$elapsed = $end - $start;
$self->do_debug("$host: TacacsPlus Authen Granted: $elapsed time");
push @times, $elapsed;
$r->close();
}else{
$self->do_log("Unable to Authenticate to:$host with ID:$username Key:$secret");
$result = "Unable to Authenticate Msg: " . Authen::TacacsPlus::errmsg();
$self->do_debug("$result");
}
}else{
$self->do_log("Unable to Create Constructor Authen::TacacsPlus for host:$host");
$result = "Unable to Build Constructor Msg: " . Authen::TacacsPlus::errmsg();
$self->do_debug("$result");
}
}
return sort { $a <=> $b } @times;
}
sub probevars {
my $class = shift;
my $h = $class->SUPER::probevars;
delete $h->{timeout};
return $class->_makevars($h, {
secretfile => {
_doc => <<DOC,
A file containing the TacacsPlus shared secrets for the targets. It should contain
whitespace-separated pairs of the form `<host> <secret>'. Comments and blank lines
are OK.
DOC
_example => '/another/place/secret',
_sub => sub {
my $val = shift;
-r $val or return "ERROR: secret file $val is not readable.";
return undef;
},
},
});
}
sub targetvars {
my $class = shift;
return $class->_makevars($class->SUPER::targetvars, {
_mandatory => [ 'username' ],
username => {
_doc => 'The username to be tested.',
_example => 'test-user',
},
password => {
_doc => 'The password for the user, if not present in the password file.',
_example => 'test-password',
},
secret => {
_doc => 'The TacacsPlus shared secret for the target, if not present in the secrets file.',
_example => 'test-secret',
},
mininterval => {
_default => $DEFAULTINTERVAL,
_doc => "The minimum interval between each authentication request sent, in (possibly fractional) seconds.",
_re => '(\d*\.)?\d+',
},
timeout => {
_default => 5,
_doc => "Timeout in seconds for the TacacsPlus queries.",
_re => '\d+',
},
port => {
_default => 49,
_doc => 'The TacacsPlus port to be used',
_re => '\d+',
_example => 49,
},
authtype => {
_default => 'ASCII',
_doc => 'The TacacsPlus Authentication type:ASCII(default), CHAP, PAP',
_re => '(ASCII|CHAP|PAP)',
_example => 'CHAP',
},
});
}
1;

View File

@@ -0,0 +1,294 @@
package Smokeping::probes::TelnetIOSPing;
=head1 301 Moved Permanently
This is a Smokeping probe module. Please use the command
C<smokeping -man Smokeping::probes::TelnetIOSPing>
to view the documentation or the command
C<smokeping -makepod Smokeping::probes::TelnetIOSPing>
to generate the POD document.
=cut
use strict;
use base qw(Smokeping::probes::basefork);
use Net::Telnet ();
use Carp;
my $e = "=";
sub pod_hash {
return {
name => <<DOC,
Smokeping::probes::TelnetIOSPing - Cisco IOS Probe for SmokePing
DOC
description => <<DOC,
Integrates Cisco IOS as a probe into smokeping. Uses the telnet protocol
to run a ping from an IOS device (source) to another device (host).
This probe basically uses the "extended ping" of the Cisco IOS. You have
the option to specify which interface the ping is sourced from as well.
DOC
notes => <<DOC,
${e}head2 IOS configuration
The IOS device should have a username/password configured, as well as
the ability to connect to the VTY(s).
eg:
!
username smokeping privilege 5 password 0 SmokepingPassword
!
line vty 0 4
login local
transport input telnet
!
Some IOS devices have a maximum of 5 VTYs available, so be careful not
to hit a limit with the 'forks' variable.
${e}head2 Requirements
This module requires the Net::Telnet module for perl. This is usually
included on most newer OSs which include perl.
${e}head2 Debugging
There is some VERY rudimentary debugging code built into this module (it's
based on the debugging code written into Net::Telnet). It will log
information into three files "TIPreturn", "TIPoutlog", and "TIPdump".
These files will be written out into your current working directory (CWD).
You can change the names of these files to something with more meaning to
you.
${e}head2 Password authentication
You should be advised that the authentication method of telnet uses
clear text transmissions...meaning that without proper network security
measures someone could sniff your username and password off the network.
I may attempt to incorporate SSH in a future version of this module, but
it is very doubtful. Right now SSH adds a LOT of processing overhead to
a router, and isn't incredibly easy to implement in perl.
Having said this, don't be too scared of telnet. Remember, the
original IOSPing module used RSH, which is even more scary to use from
a security perspective.
${e}head2 Ping packet size
The FPing manpage has the following to say on the topic of ping packet
size:
Number of bytes of ping data to send. The minimum size (normally 12)
allows room for the data that fping needs to do its work (sequence
number, timestamp). The reported received data size includes the IP
header (normally 20 bytes) and ICMP header (8 bytes), so the minimum
total size is 40 bytes. Default is 56, as in ping. Maximum is the
theoretical maximum IP datagram size (64K), though most systems limit
this to a smaller, system-dependent number.
DOC
authors => <<'DOC',
John A Jackson <geonjay@infoave.net>
based HEAVILY on Smokeping::probes::IOSPing by
Paul J Murphy <paul@murph.org>
based on Smokeping::probes::FPing by
Tobias Oetiker <tobi@oetiker.ch>
DOC
}
}
sub new($$$)
{
my $proto = shift;
my $class = ref($proto) || $proto;
my $self = $class->SUPER::new(@_);
# no need for this if we run as a cgi
unless ( $ENV{SERVER_SOFTWARE} ) {
$self->{pingfactor} = 1000; # Gives us a good-guess default
print "### assuming you are using an IOS reporting in milliseconds\n";
};
return $self;
}
sub ProbeDesc($){
my $self = shift;
my $bytes = $self->{properties}{packetsize};
return "InfoAve Cisco IOS - ICMP Echo Pings ($bytes Bytes)";
}
sub pingone ($$){
my $self = shift;
my $target = shift;
my $source = $target->{vars}{source};
my $dest = $target->{vars}{host};
my $psource = $target->{vars}{psource} || "";
my $port = 23;
my @output = ();
my $login = $target->{vars}{iosuser};
my $pssword = $target->{vars}{iospass};
my $bytes = $self->{properties}{packetsize};
my $pings = $self->pings($target);
my $timeout = $self->{properties}{timeout};
my $vrf ="";
if ( defined ($target->{vars}{vrf}) ) {
$vrf = " vrf $target->{vars}{vrf}";
}
# do NOT call superclass ... the ping method MUST be overridden
my %upd;
my @args = ();
my $telnet = Net::Telnet->new( Timeout => $timeout );
# These are for debugging
# $telnet->errmode("TIPreturn");
# $telnet->input_log("TIPinlog");
# $telnet->dump_log("TIPdumplog");
#Open the Connection to the router
# open(OUTF,">outfile.IA") || die "Can't open OUTF: $!";
# print OUTF "target => $dest\nsource => $source\nuser => $login\n";
my $ok = $telnet->open(Host => $source,
Port => $port);
# print OUTF "Connection is a $ok\n";
#Authenticate
$telnet->waitfor('/(ogin|name|word):.*$/');
$telnet->print("$login");
$telnet->waitfor('/word:.*$/');
$telnet->print("$pssword");
#Do the work
$telnet->waitfor('/[\@\w\-\.]+[>#][ ]*$/');
$telnet->print("terminal length 0");
$telnet->waitfor('/[\@\w\-\.]+[>#][ ]*$/');
$telnet->print("ping$vrf");
$telnet->waitfor('/Protocol \[ip\]: $/');
$telnet->print("");
$telnet->waitfor('/Target IP address: $/');
$telnet->print("$dest");
$telnet->waitfor('/Repeat count \[5\]: $/');
$telnet->print($pings);
$telnet->waitfor('/Datagram size \[100\]: $/');
$telnet->print("$bytes");
$telnet->waitfor('/Timeout in seconds \[2\]: $/');
$telnet->print("");
$telnet->waitfor('/Extended commands \[n\]: $/');
$telnet->print("y");
$telnet->waitfor('/Source address or interface: $/');
$telnet->print("$psource");
$telnet->waitfor('/Type of service \[0\]: $/');
$telnet->print("");
$telnet->waitfor('/Set DF bit in IP header\? \[no\]: $/');
$telnet->print("");
$telnet->waitfor('/Validate reply data\? \[no\]: $/');
$telnet->print("");
$telnet->waitfor('/Data pattern \[0xABCD\]: $/');
$telnet->print("");
$telnet->waitfor('/Loose, Strict, Record, Timestamp, Verbose\[none\]: $/');
$telnet->print("v");
$telnet->waitfor('/Loose, Strict, Record, Timestamp, Verbose\[V\]: $/');
$telnet->print("");
$telnet->waitfor('/Sweep range of sizes.+$/');
$telnet->prompt('/[\@\w\-\.]+[>#][ ]*$/');
@output = $telnet->cmd("n");
#$telnet->waitfor('/[\@\w\-\.]+[>#][ ]*$/');
$telnet->print("quit");
$telnet->close;
# print OUTF "closed Telnet connection\n";
my @times = ();
while (@output) {
my $outputline = shift @output;
chomp($outputline);
# print OUTF "$outputline\n";
$outputline =~ /^Reply to request \d+ \((\d+) ms\)/ && push(@times,$1);
#print OUTF "$outputline => $1\n";
}
@times = map {sprintf "%.10e", $_ / $self->{pingfactor}} sort {$a <=> $b} @times;
# close(OUTF);
return @times;
}
sub probevars {
my $class = shift;
return $class->_makevars($class->SUPER::probevars, {
packetsize => {
_doc => <<DOC,
The (optional) packetsize option lets you configure the packetsize for
the pings sent.
DOC
_default => 56,
_re => '\d+',
_sub => sub {
my $val = shift;
return "ERROR: packetsize must be between 12 and 64000"
unless $val >= 12 and $val <= 64000;
return undef;
},
},
});
}
sub targetvars {
my $class = shift;
return $class->_makevars($class->SUPER::targetvars, {
_mandatory => [ 'iosuser', 'iospass', 'source' ],
source => {
_doc => <<DOC,
The source option specifies the IOS device to which we telnet. This
is an IP address of an IOS Device that you/your server:
1) Have the ability to telnet to
2) Have a valid username and password for
DOC
_example => "192.168.2.1",
},
psource => {
_doc => <<DOC,
The (optional) psource option specifies an alternate IP address or
Interface from which you wish to source your pings from. Routers
can have many many IP addresses, and interfaces. When you ping from a
router you have the ability to choose which interface and/or which IP
address the ping is sourced from. Specifying an IP/interface does not
necessarily specify the interface from which the ping will leave, but
will specify which address the packet(s) appear to come from. If this
option is left out the IOS Device will source the packet automatically
based on routing and/or metrics. If this doesn't make sense to you
then just leave it out.
DOC
_example => "192.168.2.129",
},
iosuser => {
_doc => <<DOC,
The iosuser option allows you to specify a username that has ping
capability on the IOS Device.
DOC
_example => 'user',
},
iospass => {
_doc => <<DOC,
The iospass option allows you to specify the password for the username
specified with the option iosuser.
DOC
_example => 'password',
},
vrf => {
_doc => <<DOC,
The vrf option allows you to specify the vrf for ping
DOC
_example => 'VRF1',
},
});
}
1;

View File

@@ -0,0 +1,250 @@
package Smokeping::probes::TelnetJunOSPing;
=head1 301 Moved Permanently
This is a Smokeping probe module. Please use the command
C<smokeping -man Smokeping::probes::TelnetJunOSPing>
to view the documentation or the command
C<smokeping -makepod Smokeping::probes::TelnetJunOSPing>
to generate the POD document.
=cut
use strict;
use base qw(Smokeping::probes::basefork);
use Net::Telnet ();
use Carp;
my $e = "=";
sub pod_hash {
return {
name => <<DOC,
Smokeping::probes::TelnetJunOSPing - Juniper JunOS Probe for SmokePing
DOC
description => <<DOC,
Integrates Juniper JunOS as a probe into smokeping. Uses the telnet protocol
to run a ping from an JunOS device (source) to another device (host).
This probe basically uses the "extended ping" of the Juniper JunOS. You have
the option to specify which interface the ping is sourced from as well.
DOC
notes => <<DOC,
${e}head2 JunOS configuration
The JunOS device should have a username/password configured, as well as
the ability to connect to the VTY(s).
Some JunOS devices have a maximum of 5 VTYs available, so be careful not
to hit a limit with the 'forks' variable.
${e}head2 Requirements
This module requires the Net::Telnet module for perl. This is usually
included on most newer OSs which include perl.
${e}head2 Debugging
There is some VERY rudimentary debugging code built into this module (it's
based on the debugging code written into Net::Telnet). It will log
information into three files "TIPreturn", "TIPoutlog", and "TIPdump".
These files will be written out into your current working directory (CWD).
You can change the names of these files to something with more meaning to
you.
${e}head2 Password authentication
You should be advised that the authentication method of telnet uses
clear text transmissions...meaning that without proper network security
measures someone could sniff your username and password off the network.
I may attempt to incorporate SSH in a future version of this module, but
it is very doubtful. Right now SSH adds a LOT of processing overhead to
a router, and isn't incredibly easy to implement in perl.
Having said this, don't be too scared of telnet. Remember, the
original JunOSPing module used RSH, which is even more scary to use from
a security perspective.
${e}head2 Ping packet size
The FPing manpage has the following to say on the topic of ping packet
size:
Number of bytes of ping data to send. The minimum size (normally 12)
allows room for the data that fping needs to do its work (sequence
number, timestamp). The reported received data size includes the IP
header (normally 20 bytes) and ICMP header (8 bytes), so the minimum
total size is 40 bytes. Default is 56, as in ping. Maximum is the
theoretical maximum IP datagram size (64K), though most systems limit
this to a smaller, system-dependent number.
DOC
authors => <<'DOC',
S H A N <shanali@yahoo.com>
based HEAVILY on Smokeping::probes::TelnetIOSPing by
John A Jackson <geonjay@infoave.net>
based on Smokeping::probes::JunOSPing by
Paul J Murphy <paul@murph.org>
based on Smokeping::probes::FPing by
Tobias Oetiker <tobi@oetiker.ch>
DOC
}
}
sub new($$$)
{
my $proto = shift;
my $class = ref($proto) || $proto;
my $self = $class->SUPER::new(@_);
# no need for this if we run as a cgi
unless ( $ENV{SERVER_SOFTWARE} ) {
$self->{pingfactor} = 1000; # Gives us a good-guess default
print "### assuming you are using an JunOS reporting in milliseconds\n";
};
return $self;
}
sub ProbeDesc($){
my $self = shift;
my $bytes = $self->{properties}{packetsize};
return "InfoAve Juniper JunOS - ICMP Echo Pings ($bytes Bytes)";
}
sub pingone ($$){
my $self = shift;
my $target = shift;
my $source = $target->{vars}{source};
my $dest = $target->{vars}{host};
my $psource = $target->{vars}{psource};
my $port = 23;
my @output = ();
my $login = $target->{vars}{junosuser};
my $pssword = $target->{vars}{junospass};
my $bytes = $self->{properties}{packetsize};
my $pings = $self->pings($target);
# do NOT call superclass ... the ping method MUST be overridden
my %upd;
my @args = ();
my $telnet = Net::Telnet->new(Timeout => 60);
# $telnet->errmode("TIPreturn");
# $telnet->input_log("TIPinlog");
# $telnet->dump_log("TIPdumplog");
# Open the Connection to the router
# open(OUTF,">outfile.IA") || die "Can't open OUTF: $!";
# print OUTF "target => $dest\nsource => $source\nuser => $login\n";
my $ok = $telnet->open(Host => $source,
Port => $port);
# print OUTF "Connection is a $ok\n";
#Authenticate
$telnet->waitfor('/(ogin):.*$/');
$telnet->print("$login");
$telnet->waitfor('/word:.*$/');
$telnet->print("$pssword");
$telnet->prompt('/[\@\w\-\.]+[>#][ ]*$/');
#Do the work
$telnet->waitfor('/[\@\w\-\.]+[>#][ ]*$/');
$telnet->print("set cli screen-length 0");
$telnet->waitfor('/[\@\w\-\.]+[>#][ ]*$/');
if ( $psource ) {
@output = $telnet->cmd("ping $dest count $pings size $bytes source $psource");
} else {
@output = $telnet->cmd("ping $dest count $pings size $bytes");
}
$telnet->print("quit");
$telnet->close;
# print OUTF "closed Telnet connection\n";
my @times = ();
while (@output) {
my $outputline = shift @output;
chomp($outputline);
# print OUTF "$outputline\n";
$outputline =~ /^\d+ bytes from $dest: icmp_seq=\d+ ttl=\d+ time=(\d+\.\d+) ms$/ && push(@times,$1);
#print OUTF "$outputline => $1\n";
}
@times = map {sprintf "%.10e", $_ / $self->{pingfactor}} sort {$a <=> $b} @times;
# close(OUTF);
return @times;
}
sub probevars {
my $class = shift;
return $class->_makevars($class->SUPER::probevars, {
packetsize => {
_doc => <<DOC,
The (optional) packetsize option lets you configure the packetsize for
the pings sent.
DOC
_default => 100,
_re => '\d+',
_sub => sub {
my $val = shift;
return "ERROR: packetsize must be between 12 and 64000"
unless $val >= 12 and $val <= 64000;
return undef;
},
},
});
}
sub targetvars {
my $class = shift;
return $class->_makevars($class->SUPER::targetvars, {
_mandatory => [ 'junosuser', 'junospass', 'source' ],
source => {
_doc => <<DOC,
The source option specifies the JunOS device to which we telnet. This
is an IP address of an JunOS Device that you/your server:
1) Have the ability to telnet to
2) Have a valid username and password for
DOC
_example => "192.168.2.1",
},
psource => {
_doc => <<DOC,
The (optional) psource option specifies an alternate IP address or
Interface from which you wish to source your pings from. Routers
can have many many IP addresses, and interfaces. When you ping from a
router you have the ability to choose which interface and/or which IP
address the ping is sourced from. Specifying an IP/interface does not
necessarily specify the interface from which the ping will leave, but
will specify which address the packet(s) appear to come from. If this
option is left out the JunOS Device will source the packet automatically
based on routing and/or metrics. If this doesn't make sense to you
then just leave it out.
DOC
_example => "192.168.2.129",
},
junosuser => {
_doc => <<DOC,
The junosuser option allows you to specify a username that has ping
capability on the JunOS Device.
DOC
_example => 'user',
},
junospass => {
_doc => <<DOC,
The junospass option allows you to specify the password for the username
specified with the option junosuser.
DOC
_example => 'password',
},
});
}
1;

View File

@@ -0,0 +1,335 @@
package Smokeping::probes::TraceroutePing;
=head1 301 Moved Permanently
This is a Smokeping probe module. Please use the command
C<smokeping -man Smokeping::probes::TraceroutePing>
to view the documentation or the command
C<smokeping -makepod Smokeping::probes::TraceroutePing>
to generate the POD document.
=cut
use warnings;
use strict;
use base qw(Smokeping::probes::basefork);
# or, alternatively
# use base qw(Smokeping::probes::base);
use Carp;
use IPC::Open3;
use Symbol;
use Socket qw(:addrinfo);
sub pod_hash {
return {
name => <<'DOC',
Smokeping::probes::TraceroutePing - use traceroute to obtain RTT for a router
DOC
description => <<'DOC',
Integrates standard traceroute as a probe into smokeping. The use
case for this probe is gateways that do not respond to TCP/UDP/ICMP
packets addressed to them, but do return ICMP TTL_EXCEEDED packets for
traceroute packets to a host they route to. It is best used in
situations where routing for the path is static or nearly so;
attempting to use this on networks with changing routing will yield
poor results. The best place to use this probe is on first- and
last-mile links, which are more likely to have static routing and
also more likely to have firewalls that ignore ICMP ECHO_REQUEST.
The mandatory probe variable B<binary> must have an executable path for
traceroute.
The optional probe variable B<binaryv6> sets an executable path for
your IPv6 traceroute. If this is set to the same value as B<binary>,
TraceroutePing will use the -6 flag when running traceroute for IPv6
addresses. If this variable is not set, TraceroutePing will try to
find an functioning IPv6 traceroute. It will first try appending "6"
to the path in B<binary>, then try including the "-6" flag in a test
command. Note that Linux appears to have a wide variety of IPv6
traceroute implementations. My Ubuntu 14.04 machine has
/usr/sbin/traceroute6 from iputils, but /usr/bin/traceroute (from
Dmitry Butskoy) accepts the -6 flag and is actually a better
implementation. You may need to let TraceroutePing autodetect this, or
experiment to find the best traceroute.
The mandatory target variable B<desthost> must name a destination host
for the probe. The destination host itself is not of interest and no
data is gathered on it, its only purpose is to route traffic past your
actual target. Selection of a destination just past your target, with
static or strongly preferred routing through your target, will get
better data.
The mandatory target variable B<host> must name the target host for
the probe. This is the router that you want to collect RTT data for.
This variable must either be the valid reverse-lookup name of the
router, or its IP address. Using the IP address is preferable since
it allows us to tell traceroute to avoid DNS lookups.
The target variables B<minttl> and B<maxttl> can be used to describe
the range of expected hop counts to B<host>. On longer paths or paths
through unresponsive gateways or ending in unresponsive hosts, this
reduces the amount of time this probe takes to execute. These default
to 1 and 30.
The target variables B<wait> sets the traceroute probe timeout in
seconds. This defaults to 1, instead of the traditionally higher
value used by LBL traceroute. Traceroute programs often enforce a
lower bound on this value.
DOC
authors => <<'DOC',
John Hood <cgull@glup.org>,
DOC
see_also => <<'DOC'
L<smokeping_extend>
DOC
};
}
sub new($$$)
{
my $proto = shift;
my $class = ref($proto) || $proto;
my $self = $class->SUPER::new(@_);
$self->do_debug("command: $self->{properties}{binary}, " . $self->{properties}{binary});
# Do we need to find an IPv6 traceroute? We can't make it a mandatory probe variable,
# there are likely people out there still using IPv4-only OS installs.
if ($self->{properties}{binaryv6}) {
$self->do_debug("configured v6 command: $self->{properties}{binaryv6}, " .
$self->{properties}{binaryv6});
} else {
my $tail = " -n -q1 -f1 -m1 -w1 ::1 >/dev/null 2>&1";
# First try "traceroute -6 ..."
system($self->{properties}{binary} . " -6 ${tail}");
if ($? == 0) {
$self->{properties}{binaryv6} = $self->{properties}{binary};
} else {
# Then try "traceroute6 ..."
system($self->{properties}{binary} . "6 ${tail}");
if ($? == 0) {
$self->{properties}{binaryv6} = $self->{properties}{binary} . "6";
} else {
$self->{properties}{binaryv6} = "/bin/false";
}
}
$self->do_debug("discovered v6 command: $self->{properties}{binaryv6}, " .
$self->{properties}{binaryv6});
}
return $self;
}
sub ProbeDesc($){
my $self = shift;
return "Traceroute (UDP + TTL) Pings";
}
# Probe-specific variables.
sub probevars {
my $class = shift;
return $class->_makevars($class->SUPER::probevars, {
_mandatory => [ 'binary' ],
binary => {
_doc => "The location of your traceroute binary.",
_example => '/usr/bin/traceroute',
_sub => sub {
my $val = shift;
return "ERROR: traceroute '$val' does not point to an executable"
unless -f $val and -x _;
return undef;
},
},
binaryv6 => {
_doc => "The location of your IPv6 traceroute binary.",
_example => '/usr/bin/traceroute6',
_sub => sub {
my $val = shift;
return "ERROR: IPv6 traceroute '$val' does not point to an executable"
unless -f $val and -x _;
return undef;
},
},
});
}
# Target-specific variables.
sub targetvars {
my $class = shift;
return $class->_makevars($class->SUPER::targetvars, {
_mandatory => [ 'desthost', 'host' ],
desthost => {
_doc => "Final destination host for traceroute packets. Does not have to be reachable unless it is also your host.",
_example => 'www.example.com',
_sub => sub {
my $val = shift;
return undef;
},
},
host => {
_doc => "Host of interest to monitor. Must be either the host's reverse-lookup name, or an IP address.",
_example => 'www-net-router.example.com',
_sub => sub {
my $val = shift;
return undef;
},
},
minttl => {
_doc => "Minimum TTL. Set to the minimum expected number of hops to host.",
_example => '11',
_sub => sub {
my $val = shift;
return undef;
}
},
maxttl => {
_doc => "Maximum TTL. Set to the maximum expected number of hops to host.",
_example => '15',
_sub => sub {
my $val = shift;
return undef;
},
},
wait => {
_doc => "Waittime. The timeout value for traceroute's probes, in seconds.",
_example => '3',
_sub => sub {
my $val = shift;
return undef;
},
},
});
}
sub pingone ($) {
my $self = shift;
my $target = shift;
# Defaults
my $minttl = 1;
my $maxttl = 30;
my $wait = 1;
# Fish out args
my $binary = $self->{properties}{binary};
# my $weight = $target->{vars}{weight}
my $count = $self->pings($target); # the number of pings for this target
my $desthost = $target->{vars}{desthost};
my $host = $target->{vars}{host};
$minttl = $target->{vars}{minttl} if $target->{vars}{minttl};
$maxttl = $target->{vars}{maxttl} if $target->{vars}{maxttl};
$wait = $target->{vars}{wait} if $target->{vars}{wait};
# Check host and desthost for numericness and IPv6
$self->do_debug("Host $host");
my %hints = ( flags => Socket::AI_NUMERICHOST );
my ($err, @res) = getaddrinfo($host, 0, \%hints);
my $use_numeric = ! $err;
($err, @res) = getaddrinfo($host);
return if $err;
my $hostinfo = $res[0];
my $v6 = $hostinfo->{family} eq Socket::AF_INET6;
$self->do_debug("Desthost $desthost");
($err, @res) = getaddrinfo($desthost);
return if $err;
my $destinfo = $res[0];
my $destv6 = $destinfo->{family} eq Socket::AF_INET6;
# Validate them
if ($v6 != $destv6) {
$self->do_debug("address families don't match, $host $desthost");
return;
}
$self->do_debug("validated $host");
# ping one target
my @cmd;
if (!$v6) {
push @cmd, $self->{properties}{binary};
} else {
push @cmd, $self->{properties}{binaryv6};
my $same_binaries = $self->{properties}{binaryv6} eq $self->{properties}{binary};
if ($same_binaries) {
push @cmd, "-6";
}
}
push @cmd, (
'-w', $wait,
'-f', $minttl,
'-m', $maxttl,
'-q', '1',
);
push(@cmd, "-n") if $use_numeric;
push(@cmd, $desthost);
# Run traceroute for only one iteration in an external loop, to
# avoid various parsing problems that can come up with >1 iteration.
my @times;
for (1..$count) {
$self->do_debug("Executing @cmd");
my $killed;
my $f_stdin = gensym;
my $f_stdout = gensym;
my $f_stderr = gensym;
my $pid = open3($f_stdin, $f_stdout, $f_stderr, @cmd);
while (<$f_stdout>){
my $line = $_;
chomp($line);
$self->do_debug("stdout: $line");
next unless $line =~ /^\s*\d+\s+\S+\s+[\d\.,]+\s+\S+\s*$/; # only match RTT output
my @fields = split(/\s+/,$line);
shift @fields if $fields[0] eq ''; # discard empty first field
$self->do_debug("fields: " . join(', ',@fields));
next unless $host eq $fields[1];
shift @fields if !$use_numeric; # discard hostnames to get fields in the same position
my $time = $fields[2];
# Adjust time units to smokeping's preferred units. One
# enhanced LBL implementation has a -u option for microseconds.
for ($fields[3]) {
/^s(|ec(|ond))$/i && do { next; };
/^(m|milli)s(|ec(|ond))$/i && do { $time /= 1000; next; };
/^(u|micro)s(|ec(|ond))$/i && do { $time /= 1000000; next; };
$time /= 1000; # default
};
$self->do_debug("time: $time");
push @times, $time;
# now we have a time to our target $host-- there's no
# point in waiting for traceroute to finish the trace to
# $desthost
$killed = kill(15, $pid);
last;
}
while (<$f_stderr>){
my $line = $_;
chomp($line);
$self->do_debug("stderr: $line");
}
waitpid $pid,0;
my $rc = $?;
carp join(" ",@cmd) . " returned with exit code $rc. run with debug enabled to get more information" unless ($rc == 0 || $killed);
close $f_stdin;
close $f_stdout;
close $f_stderr;
}
@times = sort {$a <=> $b} @times;
$self->do_debug("Times: " . join(' ', @times));
return @times;
}
# That's all, folks!
1;

View File

@@ -0,0 +1,171 @@
package Smokeping::probes::WebProxyFilter;
=head1 301 Moved Permanently
This is a Smokeping probe module. Please use the command
C<smokeping -man Smokeping::probes::WebProxyFilter>
to view the documentation or the command
C<smokeping -makepod Smokeping::probes::WebProxyFilter>
to generate the POD document.
=cut
use strict;
use base qw(Smokeping::probes::basefork);
use LWP::UserAgent;
use Time::HiRes qw(gettimeofday sleep);
use Carp;
my $DEFAULTINTERVAL = 1;
sub pod_hash {
return {
name => <<DOC,
Smokeping::probes::WebProxyFilter - tests webproxy filter performance and function.
DOC
overview => <<DOC,
This probe tests if your filtering webproxy is working properly. Drawing from
a list of forbidden websites, it tries to establish a connection to
each one of them and registers a 'loss' when it succeeds.
If you want to test availability of a website, use the EchoPingHttp probe.
DOC
description => <<DOC,
The probe uses the LWP::UserAgent module to retrieve a series of webpages. It
expects to get the firewalls 'site-prohibited' page. Any other response (or
a real loss) gets logged as a loss and can be used to trigger an alarm.
The probe tries to be nice to the firewall and waits at least X seconds
between starting filetransfers, where X is the value of the probe
specific `min_interval' variable ($DEFAULTINTERVAL by default).
Many variables can be specified either in the probe or in the target definition,
the target-specific variable will override the prove-specific variable.
DOC
authors => <<'DOC',
Tobias Oetiker <tobi@oetiker.ch> sponsored by Virtela
DOC
bugs => <<DOC,
This probe is somewhat unortodox, since it regards the successful retrieval
of a banned webpage as a loss.
DOC
}
}
sub ProbeDesc ($) {
my $self = shift;
return sprintf("HTTP GETs");
}
sub new {
my $proto = shift;
my $class = ref($proto) || $proto;
my $self = $class->SUPER::new(@_);
return $self;
}
sub pingone {
my $self = shift;
my $target = shift;
my $host = $target->{addr};
my $vars = $target->{vars};
my $mininterval = $self->{properties}{min_interval};
my @times;
my $elapsed;
my $ua = LWP::UserAgent->new;
$ua->agent($vars->{useragent});
$ua->timeout($vars->{timeout});
$ua->max_size($vars->{max_size});
my @targets = ($host, split /\s*,\s*/, $vars->{more_hosts});
my $targcount = scalar @targets;
my $pingcount = $self->pings($target);
my $deny_re = $vars->{deny_re};
if ($targcount > $self->pings($target)) {
$self->do_log("ERROR There are more host addresses ($targcount) than ping slots ($pingcount), either increase the pings or reduce the targets.\n");
return;
}
for (1..$pingcount) {
if (defined $elapsed) {
my $timeleft = $mininterval - $elapsed;
sleep $timeleft if $timeleft > 0;
}
my $target = shift @targets;
push @targets,$target;
my $start = gettimeofday();
my $response = $ua->get("http://$target");
my $end = gettimeofday();
if ($response->is_success){
if ($response->content =~ /$deny_re/ism){
push @times,($end-$start);
} else {
my $content = substr($response->content,0,80)." ...";
$content =~ s/[\n\r]/ /g;
$self->do_log("Warning: Problem with target $host: got unexpected content from $target: $content");
}
} else {
$self->do_log("Warning: Problem with target $host: got this error from $target: ".$response->status_line);
}
}
return sort { $a <=> $b } @times;
}
sub probevars {
my $class = shift;
my $h = $class->SUPER::probevars;
return $class->_makevars($h, {
_mandatory => ['deny_re'],
min_interval => {
_default => $DEFAULTINTERVAL,
_doc => "The minimum interval between each starting GETs in seconds.",
_re => '(\d*\.)?\d+',
_example => '0.1'
},
useragent => {
_default => "SmokePing/2.x (WebProxyFilter Probe)",
_doc => "The web browser we claim to be, just in case the FW is interested"
},
maxsize => {
_default => 2000,
_doc => "How much of the webpage should be retrieved."
},
});
}
sub targetvars {
my $class = shift;
return $class->_makevars($class->SUPER::targetvars, {
timeout => {
_default => 2,
_doc => "Timeout in seconds for the test complete.",
_re => '\d+',
_example => 2,
},
deny_re => {
_doc => "Regular expression, matching the 'deny' response from the firewall",
_example => 'Access Prohibited',
},
more_hosts => {
_doc => <<DOC,
A comma separated list of banned websites to test in addition to the one
specified in the I<host> variable. The websites will be tested one after the
other in one round, this means that while normal probes do run the same test
several times in a row, this one will alter the webpage with each round.
The reason for this is, that even though we try to retrieve remote webpages,
the answer will come from the firewall every time, so we kill two birds in
one go. First we test the firewalls latency and second we make sure its
filter works properly.
DOC
_re => '[^\s.]+(?:\.[^\s.]+)*(\s*,[^\s.]+(?:\.[^\s.]+)*)*',
_example => 'www.playboy.com, www.our-competition.com',
},
});
}
1;

View File

@@ -0,0 +1,455 @@
package Smokeping::probes::base;
=head1 301 Moved Permanently
This is a Smokeping probe module. Please use the command
C<smokeping -man Smokeping::probes::base>
to view the documentation or the command
C<smokeping -makepod Smokeping::probes::base>
to generate the POD document.
=cut
use vars qw($VERSION);
use Carp;
use lib qw(..);
use Smokeping;
$VERSION = 1.0;
use strict;
sub pod_hash {
return {
name => <<DOC,
Smokeping::probes::base - Base Class for implementing SmokePing Probes
DOC
overview => <<DOC,
For the time being, please use the L<Smokeping::probes::FPing|Smokeping::probes::FPing> for
inspiration when implementing your own probes.
DOC
authors => <<'DOC',
Tobias Oetiker <tobi@oetiker.ch>
DOC
};
}
sub pod {
my $class = shift;
my $pod = "";
my $podhash = $class->pod_hash;
$podhash->{synopsis} = $class->pod_synopsis;
$podhash->{variables} = $class->pod_variables;
for my $what (qw(name overview synopsis description variables authors notes bugs see_also)) {
my $contents = $podhash->{$what};
next if not defined $contents or $contents eq "";
my $headline = uc $what;
$headline =~ s/_/ /; # see_also => SEE ALSO
$pod .= "=head1 $headline\n\n";
$pod .= $contents;
chomp $pod;
$pod .= "\n\n";
}
$pod .= "=cut";
return $pod;
}
sub new($$)
{
my $this = shift;
my $class = ref($this) || $this;
my $self = { properties => shift, cfg => shift,
name => shift,
targets => {}, rtts => {}, addrlookup => {}, rounds_count => 0};
bless $self, $class;
return $self;
}
sub add($$)
{
my $self = shift;
my $tree = shift;
$self->{target_count}++; # increment this anyway
return if defined $tree->{nomasterpoll} and $tree->{nomasterpoll} eq "yes";
$self->{targets}{$tree} = shift;
}
sub ping($)
{
croak "this must be overridden by the subclass";
}
sub round ($) {
return sprintf "%.0f", $_[0];
}
sub ProbeDesc ($) {
return "Probe which does not override the ProbeDesc method";
}
sub ProbeUnit ($) {
return "Seconds";
}
# this is a read-only variable that should get incremented by
# the ping() method
sub rounds_count ($) {
my $self = shift;
return $self->{rounds_count};
}
sub increment_rounds_count ($) {
my $self = shift;
$self->{rounds_count}++;
}
sub target2dynfile ($$) {
# the targets are stored in the $self->{targets}
# hash as filenames pointing to the RRD files
#
# now that we use a (optionally) different dir for the
# . adr files, we need to derive the .adr filename
# from the RRD filename with a simple substitution
my $self = shift;
my $target = shift; # filename with <datadir> embedded
my $dyndir = $self->{cfg}{General}{dyndir};
return $target unless defined $dyndir; # nothing to do
my $datadir = $self->{cfg}{General}{datadir};
$target =~ s/^\Q$datadir\E/$dyndir/;
return $target;
}
sub rrdupdate_string($$)
{ my $self = shift;
my $tree = shift;
# print "$tree -> ", join ",", @{$self->{rtts}{$tree}};print "\n";
# skip invalid addresses
my $pings = $self->_pings($tree);
return "U:${pings}:".(join ":", map {"U"} 1..($pings+1))
unless defined $self->{rtts}{$tree} and @{$self->{rtts}{$tree}} > 0;
my $entries = scalar @{$self->{rtts}{$tree}};
my @times = @{$self->{rtts}{$tree}};
my $loss = $pings - $entries;
my $median = $times[int($entries/2)] || 'U';
# shift the data into the middle of the times array
my $lowerloss = int($loss/2);
my $upperloss = $loss - $lowerloss;
@times = ((map {'U'} 1..$lowerloss),@times, (map {'U'} 1..$upperloss));
my $age;
my $dynbase = $self->target2dynfile($self->{targets}{$tree});
if ( -f $dynbase.".adr" ) {
$age = time - (stat($dynbase.".adr"))[9];
} else {
$age = 'U';
}
if ( $entries == 0 ){
$self->do_log("Warning: got zero answers from $tree->{addr}($tree->{probe}) $self->{targets}{$tree}");
$age = 'U';
$loss = 'U';
if ( -f $dynbase.".adr"
and not -f $dynbase.".snmp" ){
unlink $dynbase.".adr";
}
} ;
return "${age}:${loss}:${median}:".(join ":", @times);
}
sub addresses($)
{
my $self = shift;
my $addresses = [];
$self->{addrlookup} = {};
foreach my $tree (keys %{$self->{targets}}){
my $target = $self->{targets}{$tree};
if ($target =~ m|/|) {
my $dynbase = $self->target2dynfile($target);
if ( open D, "<$dynbase.adr" ) {
my $ip;
chomp($ip = <D>);
close D;
if ( open D, "<$dynbase.snmp" ) {
my $snmp = <D>;
chomp($snmp);
if ($snmp ne Smokeping::snmpget_ident $ip) {
# something fishy snmp properties do not match, skip this address
next;
}
close D;
}
$target = $ip;
} else {
# can't read address file skip
next;
}
}
$self->{addrlookup}{$target} = ()
unless defined $self->{addrlookup}{$target};
push @{$self->{addrlookup}{$target}}, $tree;
push @{$addresses}, $target;
};
return $addresses;
}
sub debug {
my $self = shift;
my $newval = shift;
$self->{debug} = $newval if defined $newval;
return $self->{debug};
}
sub do_debug {
my $self = shift;
return unless $self->debug;
$self->do_log(@_);
}
sub do_fatal {
my $self = shift;
$self->do_log("Fatal:", @_);
croak(@_);
}
sub do_log {
my $self = shift;
Smokeping::do_log("$self->{name}:", @_);
}
sub report {
my $self = shift;
my $count = $self->target_count;
my $offset = $self->offset_in_seconds;
my $step = $self->step;
$self->do_log("probing $count targets with step $step s and offset $offset s.");
}
sub step {
my $self = shift;
my $rv = $self->{cfg}{Database}{step};
unless (defined $self->{cfg}{General}{concurrentprobes}
and $self->{cfg}{General}{concurrentprobes} eq 'no') {
$rv = $self->{properties}{step} if defined $self->{properties}{step};
}
return $rv;
}
sub offset {
my $self = shift;
my $rv = $self->{cfg}{General}{offset};
unless (defined $self->{cfg}{General}{concurrentprobes}
and $self->{cfg}{General}{concurrentprobes} eq 'no') {
$rv = $self->{properties}{offset} if defined $self->{properties}{offset};
}
return $rv;
}
sub offset_in_seconds {
# returns the offset in seconds rather than as a percentage
# this is filled in from the initialization in Smokeping::main
my $self = shift;
my $newval = shift;
$self->{offset_in_seconds} = $newval if defined $newval;
return $self->{offset_in_seconds};
}
# the "public" method that takes a "target" argument is used by the probes
# the "private" method that takes a "tree" argument is used by Smokeping.pm
# there's no difference between them here, but we have to provide both
sub pings {
my $self = shift;
my $target = shift;
# $target is not used; basefork.pm overrides this method to provide a target-specific parameter
my $rv = $self->{cfg}{Database}{pings};
$rv = $self->{properties}{pings} if defined $self->{properties}{pings};
return $rv;
}
sub _pings {
my $self = shift;
my $tree = shift;
# $tree is not used; basefork.pm overrides this method to provide a target-specific parameter
my $rv = $self->{cfg}{Database}{pings};
$rv = $self->{properties}{pings} if defined $self->{properties}{pings};
return $rv;
}
sub target_count {
my $self = shift;
$self->{target_count} = 0 if !defined $self->{target_count};
return $self->{target_count};
}
sub probevars {
return {
step => {
_re => '\d+',
_example => 300,
_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'",
_example => '50%',
_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 acquisition is initiated.
(This variable is only applicable if the variable 'concurrentprobes' is set
in the 'General' section.)
DOC
},
pings => {
_re => '\d+',
_sub => sub {
my $val = shift;
return "ERROR: The pings value must be at least 3."
if $val < 3;
return undef;
},
_example => 20,
_doc => <<DOC,
How many pings should be sent to each target, if different from the global
value specified in the Database section. 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
},
_mandatory => [],
};
}
sub targetvars {
return {_mandatory => [],
'/^influx_.+/' => {
_re => '.*',
_example => 'influx_location = In the basement',
_doc => <<DOC,
This is a tag that will be sent to influxdb and has no impact on the probe measurement. The tag name will be sent without the "influx_" prefix, which will be replaced with "tag_" instead. Tags can be used for filtering.
DOC
},
};
}
# a helper method that combines two var hash references
# and joins their '_mandatory' lists.
sub _makevars {
my ($class, $from, $to) = @_;
for (keys %$from) {
if ($_ eq '_mandatory') {
push @{$to->{_mandatory}}, @{$from->{$_}};
next;
}
$to->{$_} = $from->{$_};
}
return $to;
}
sub pod_synopsis {
my $class = shift;
my $classname = ref $class||$class;
$classname =~ s/^Smokeping::probes:://;
my $probevars = $class->probevars;
my $targetvars = $class->targetvars;
my $pod = <<DOC;
*** Probes ***
+$classname
DOC
$pod .= $class->_pod_synopsis($probevars);
my $targetpod = $class->_pod_synopsis($targetvars);
$pod .= "\n # The following variables can be overridden in each target section\n$targetpod"
if defined $targetpod and $targetpod ne "";
$pod .= <<DOC;
# [...]
*** Targets ***
probe = $classname # if this should be the default probe
# [...]
+ mytarget
# probe = $classname # if the default probe is something else
host = my.host
DOC
$pod .= $targetpod
if defined $targetpod and $targetpod ne "";
return $pod;
}
# synopsis for one hash ref
sub _pod_synopsis {
my $class = shift;
my $vars = shift;
my %mandatory;
$mandatory{$_} = 1 for (@{$vars->{_mandatory}});
my $pod = "";
for (sort keys %$vars) {
next if /^_mandatory$/;
my $val = $vars->{$_}{_example};
$val = $vars->{$_}{_default}
if exists $vars->{$_}{_default}
and not defined $val;
$pod .= " $_ = $val";
$pod .= " # mandatory" if $mandatory{$_};
$pod .= "\n";
}
return $pod;
}
sub pod_variables {
my $class = shift;
my $probevars = $class->probevars;
my $pod = "Supported probe-specific variables:\n\n";
$pod .= $class->_pod_variables($probevars);
return $pod;
}
sub _pod_variables {
my $class = shift;
my $vars = shift;
my $pod = "=over\n\n";
my %mandatory;
$mandatory{$_} = 1 for (@{$vars->{_mandatory}});
for (sort keys %$vars) {
next if /^_mandatory$/;
$pod .= "=item $_\n\n";
$pod .= $vars->{$_}{_doc};
chomp $pod;
$pod .= "\n\n";
$pod .= "Example value: " . $vars->{$_}{_example} . "\n\n"
if exists $vars->{$_}{_example};
$pod .= "Default value: " . $vars->{$_}{_default} . "\n\n"
if exists $vars->{$_}{_default};
$pod .= "This setting is mandatory.\n\n"
if $mandatory{$_};
}
$pod .= "=back\n\n";
return $pod;
}
1;

View File

@@ -0,0 +1,278 @@
package Smokeping::probes::basefork;
=head1 301 Moved Permanently
This is a Smokeping probe module. Please use the command
C<smokeping -man Smokeping::probes::basefork>
to view the documentation or the command
C<smokeping -makepod Smokeping::probes::basefork>
to generate the POD document.
=cut
use strict;
use base qw(Smokeping::probes::basevars);
use Symbol;
use Carp;
use IO::Select;
use POSIX; # for ceil() and floor()
use Config; # for signal names
my $DEFAULTFORKS = 5;
sub pod_hash {
return {
name => <<DOC,
Smokeping::probes::basefork - Yet Another Base Class for implementing SmokePing Probes
DOC
overview => <<DOC,
Like Smokeping::probes::basevars, but supports the probe-specific property `forks'
to determine how many processes should be run concurrently. The
targets are pinged one at a time, and the number of pings sent can vary
between targets.
DOC
description => <<DOC,
Not all pinger programs support testing multiple hosts in a single go like
fping(1). If the measurement takes long enough, there may be not enough time
perform all the tests in the time available. For example, if the test takes
30 seconds, measuring ten hosts already fills up the SmokePing default
five minute step.
Thus, it may be necessary to do some of the tests concurrently. This module
defines the B<ping> method that forks the requested number of concurrent
processes and calls the B<pingone> method that derived classes must provide.
The B<pingone> method is called with one argument: a hash containing
the target that is to be measured. The contents of the hash are
described in I<Smokeping::probes::basevars>(3pm).
The number of concurrent processes is determined by the probe-specific
variable `forks' and is $DEFAULTFORKS by default. If there are more
targets than this value, another round of forks is done after the first
processes are finished. This continues until all the targets have been
tested.
The timeout in which each child has to finish is set to 5 seconds
multiplied by the maximum number of 'pings' of the targets. You can set
the base timeout differently if you want to, using the timeout property
of the probe in the master config file (this again will be multiplied
by the maximum number of pings). The probe itself can also provide
another default value if desired by modifying the _default value of
the timeout variable.
If the child isn't finished when the timeout occurs, it
will be killed along with any processes it has started.
The number of pings sent can be specified in the target-specific variable
'pings'.
DOC
authors => <<'DOC',
Niko Tyni <ntyni@iki.fi>
DOC
see_also => <<DOC,
L<Smokeping::probes::basevars>, L<Smokeping::probes::EchoPing>
DOC
}
}
my %signo;
my @signame;
{
# from perlipc man page
my $i = 0;
defined $Config{sig_name} || die "No sigs?";
foreach my $name (split(' ', $Config{sig_name})) {
$signo{$name} = $i;
$signame[$i] = $name;
$i++;
}
}
die("Missing TERM signal?") unless exists $signo{TERM};
die("Missing KILL signal?") unless exists $signo{KILL};
sub pingone {
croak "pingone: this must be overridden by the subclass";
}
sub probevars {
my $class = shift;
my $h = $class->SUPER::probevars;
delete $h->{pings};
return $class->_makevars($h, {
forks => {
_re => '\d+',
_example => 5,
_doc => "Run this many concurrent processes at maximum",
_default => $DEFAULTFORKS,
},
timeout => {
_re => '\d+',
_example => 15,
_default => 5,
_doc => "How long a single 'ping' takes at maximum",
},
});
}
sub targetvars {
my $class = shift;
return $class->_makevars($class->SUPER::targetvars, {
pings => {
_re => '\d+',
_sub => sub {
my $val = shift;
return "ERROR: The pings value must be at least 3."
if $val < 3;
return undef;
},
_example => 5,
_doc => <<DOC,
How many pings should be sent to each target, if different from the global
value specified in the Database section. 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
},
});
}
sub ping {
my $self = shift;
# increment the internal 'rounds' counter
$self->increment_rounds_count;
my @targets = @{$self->targets};
return unless @targets;
my $forks = $self->{properties}{forks};
my $maxpings = 0;
my $maxtimeout = $self->{properties}{timeout};
for (@targets) {
my $p = $self->pings($_);
$maxpings = $p if $p > $maxpings;
# some probes have a target-specific timeout variable
# dig out the maximum timeout
my $t = $_->{vars}{timeout};
$maxtimeout = $t if $t > $maxtimeout;
}
# we add 1 so that the probes doing their own timeout handling
# have time to do it even in the worst case
my $timeout = $maxpings * $maxtimeout + 1;
$self->{rtts}={};
$self->do_debug("forks $forks, timeout for each target $timeout");
while (@targets) {
my %targetlookup;
my %pidlookup;
my $s = IO::Select->new();
my $starttime = time();
for (1..$forks) {
last unless @targets;
my $t = pop @targets;
my $pid;
my $handle = gensym;
my $sleep_count = 0;
do {
$pid = open($handle, "-|");
unless (defined $pid) {
$self->do_log("cannot fork: $!");
$self->fatal("bailing out")
if $sleep_count++ > 6;
sleep 10;
}
} until defined $pid;
if ($pid) { #parent
$s->add($handle);
$targetlookup{$handle} = $t;
$pidlookup{$handle} = $pid;
} else { #child
# we detach from the parent's process group
setpgrp(0, $$);
# re-initialize the RNG for each subprocess
srand(time()+$$);
my @times = $self->pingone($t);
print join(" ", @times), "\n";
exit;
}
}
my $timeleft = $timeout - (time() - $starttime);
while ($s->handles and $timeleft > 0) {
for my $ready ($s->can_read($timeleft)) {
$s->remove($ready);
my $response = <$ready>;
close $ready;
chomp $response;
my @times = split(/ /, $response);
my $target = $targetlookup{$ready};
my $tree = $target->{tree};
$self->{rtts}{$tree} = \@times;
$self->do_debug("$target->{addr}: got $response");
}
$timeleft = $timeout - (time() - $starttime);
}
my @left = $s->handles;
for my $handle (@left) {
$self->do_log("$targetlookup{$handle}{addr}: timeout ($timeout s) reached, killing the probe.");
# we kill the child's process group (negative signal)
# this should finish off the actual pinger process as well
my $pid = $pidlookup{$handle};
kill -$signo{TERM}, $pid;
sleep 1;
kill -$signo{KILL}, $pid;
close $handle;
$s->remove($handle);
}
}
}
# the "private" method that takes a "tree" argument is used by Smokeping.pm
sub _pings {
my $self = shift;
my $tree = shift;
my $vars = $self->vars($tree);
return $vars->{pings} if defined $vars->{pings};
return $self->SUPER::pings();
}
# the "public" method that takes a "target" argument is used by the probes
sub pings {
my $self = shift;
my $target = shift;
return $self->SUPER::pings() unless ref $target;
return $self->_pings($target->{tree});
}
sub ProbeDesc {
return "Probe that can fork and doesn't override the ProbeDesc method";
}
sub pod_variables {
my $class = shift;
my $pod = $class->SUPER::pod_variables;
my $targetvars = $class->targetvars;
$pod .= "Supported target-specific variables:\n\n";
$pod .= $class->_pod_variables($targetvars);
return $pod;
}
1;

View File

@@ -0,0 +1,105 @@
package Smokeping::probes::basevars;
=head1 301 Moved Permanently
This is a Smokeping probe module. Please use the command
C<smokeping -man Smokeping::probes::basevars>
to view the documentation or the command
C<smokeping -makepod Smokeping::probes::basevars>
to generate the POD document.
=cut
use strict;
use Smokeping::probes::base;
use base qw(Smokeping::probes::base);
my $e = "=";
sub pod_hash {
return {
name => <<DOC,
Smokeping::probes::basevars - Another Base Class for implementing SmokePing Probes
DOC
overview => <<DOC,
Like L<Smokeping::probes::base|Smokeping::probes::base>, but supports host-specific variables for the probe.
DOC
description => <<DOC,
Provides the method `targets' that returns a list of hashes.
The hashes contain the entries:
${e}over
${e}item addr
The address of the target.
${e}item vars
A hash containing variables defined in the corresponding
config section.
${e}item tree
The unique index that `probe::base' uses for targets.
There's also the method 'vars' that returns the above mentioned
hash corresponding to the 'tree' index parameter.
${e}back
DOC
authors => <<'DOC',
Niko Tyni <ntyni@iki.fi>
DOC
bugs => <<DOC,
Uses `Smokeping::probes::base' internals too much to be a derived class, but
I didn't want to touch the base class directly.
DOC
see_also => <<DOC,
L<Smokeping::probes::base>, L<Smokeping::probes::EchoPing>
DOC
}
}
sub add($$)
{
my $self = shift;
my $tree = shift;
$self->{target_count}++;
$self->{targets}{$tree} = shift;
$self->{vars}{$tree} = { %{$self->{properties}}, %$tree };
}
sub targets {
my $self = shift;
my $addr = $self->addresses;
my @targets;
# copy the addrlookup lists to safely pop
my %copy;
for (@$addr) {
@{$copy{$_}} = @{$self->{addrlookup}{$_}} unless exists $copy{$_};
my $tree = pop @{$copy{$_}};
my $vars = $self->{vars}{$tree};
next if defined $vars->{nomasterpoll} and $vars->{nomasterpoll} eq "yes";
push @targets, { addr => $_, vars => $vars, tree => $tree };
}
return \@targets;
}
sub vars {
my $self = shift;
my $tree = shift;
return $self->{vars}{$tree};
}
sub ProbeDesc {
return "Probe that supports variables and doesn't override the ProbeDesc method";
}
return 1;

View File

@@ -0,0 +1,148 @@
package Smokeping::probes::passwordchecker;
=head1 301 Moved Permanently
This is a Smokeping probe module. Please use the command
C<smokeping -man Smokeping::probes::passwordchecker>
to view the documentation or the command
C<smokeping -makepod Smokeping::probes::passwordchecker>
to generate the POD document.
=cut
use strict;
use Smokeping::probes::basefork;
use base qw(Smokeping::probes::basefork);
use Carp;
my $e = "=";
sub pod_hash {
return {
name => <<DOC,
Smokeping::probes::passwordchecker - A Base Class for implementing SmokePing Probes
DOC
overview => <<DOC,
Like Smokeping::probes::basefork, but supports a probe-specific configuration file
for storing passwords and a method for accessing them.
DOC
description => <<DOC,
${e}head2 synopsis with more detail
SmokePing main configuration file:
*** Probes ***
+ MyPasswordChecker
# location of the file containing usernames and passwords
passwordfile = /usr/share/smokeping/etc/passwords
The specified password file:
# host:username:password
host1:joe:hardlyasecret
# comments and whitespace lines are allowed
host2:sue:notasecreteither
${e}head2 Actual description
In implementing authentication probes, it might not be desirable to store
the necessary cleartext passwords in the SmokePing main configuration
file, since the latter must be readable both by the SmokePing daemon
performing the probes and the CGI that displays the results. If the
passwords are stored in a different file, this file can be made readable
by only the user the daemon runs as. This way we can be sure that nobody
can trick the CGI into displaying the passwords on the Web.
This module reads the passwords in at startup from the file specified
in the probe-specific variable `passwordfile'. The passwords can later
be accessed and modified by the B<password> method, that needs the corresponding
host and username as arguments.
${e}head2 Password file format
The password file format is simply one line for each triplet of host,
username and password, separated from each other by colons (:).
Comment lines, starting with the `#' sign, are ignored, as well as
empty lines.
DOC
authors => <<'DOC',
Niko Tyni <ntyni@iki.fi>
DOC
bugs => <<DOC,
The need for storing cleartext passwords can be considered a bug in itself.
DOC
see_also => <<DOC,
L<Smokeping::probes::basefork>, L<Smokeping::probes::Radius>, L<Smokeping::probes::LDAP>
DOC
}
}
sub ProbeDesc {
return "probe that can fork, knows about passwords and doesn't override the ProbeDesc method";
}
sub probevars {
my $class = shift;
return $class->_makevars($class->SUPER::probevars, {
passwordfile => {
_doc => "Location of the file containing usernames and passwords.",
_example => '/some/place/secret',
_sub => sub {
my $val = shift;
-r $val or $ENV{SERVER_SOFTWARE} or return "ERROR: password file $val is not readable.";
return undef;
},
},
});
}
sub new {
my $proto = shift;
my $class = ref($proto) || $proto;
my $self = $class->SUPER::new(@_);
# no need for this if we run as a cgi
unless ($ENV{SERVER_SOFTWARE}) {
if (defined $self->{properties}{passwordfile}) {
my @stat = stat($self->{properties}{passwordfile});
my $mode = $stat[2];
carp("Warning: password file $self->{properties}{passwordfile} is world-readable\n")
if defined $mode and $mode & 04;
open(P, "<$self->{properties}{passwordfile}")
or croak("Error opening specified password file $self->{properties}{passwordfile}: $!");
while (<P>) {
chomp;
next unless /\S/;
next if /^\s*#/;
my ($host, $username, $password) = split(/:/);
carp("Line $. in $self->{properties}{passwordfile} is invalid"), next unless defined $host and defined $username and defined $password;
$self->password($host, $username, $password);
}
close P;
}
}
return $self;
}
sub password {
my $self = shift;
my $host = shift;
my $username = shift;
my $newval = shift;
$self->{password}{$host}{$username} = $newval if defined $newval;
return $self->{password}{$host}{$username};
}
1;

View File

@@ -0,0 +1,134 @@
package Smokeping::probes::skel;
=head1 301 Moved Permanently
This is a Smokeping probe module. Please use the command
C<smokeping -man Smokeping::probes::skel>
to view the documentation or the command
C<smokeping -makepod Smokeping::probes::skel>
to generate the POD document.
=cut
use strict;
use base qw(Smokeping::probes::basefork);
# or, alternatively
# use base qw(Smokeping::probes::base);
use Carp;
sub pod_hash {
return {
name => <<DOC,
Smokeping::probes::skel - a skeleton for Smokeping Probes
DOC
description => <<DOC,
This is a non-functional module that is intended to act as a
basis for creation of new probes. See the L<smokeping_extend>
document for more information.
DOC
authors => <<'DOC',
Niko Tyni <ntyni@iki.fi>,
DOC
see_also => <<DOC
L<smokeping_extend>
DOC
};
}
sub new($$$)
{
my $proto = shift;
my $class = ref($proto) || $proto;
my $self = $class->SUPER::new(@_);
# no need for this if we run as a cgi
unless ( $ENV{SERVER_SOFTWARE} ) {
# if you have to test the program output
# or something like that, do it here
# and bail out if necessary
};
return $self;
}
# This is where you should declare your probe-specific variables.
# The example shows the common case of checking the availability of
# the specified binary.
sub probevars {
my $class = shift;
return $class->_makevars($class->SUPER::probevars, {
#_mandatory => [ 'binary' ],
#binary => {
# _doc => "The location of your pingpong binary.",
# _example => '/usr/bin/pingpong',
# _sub => sub {
# my $val = shift;
# return "ERROR: pingpong 'binary' does not point to an executable"
# unless -f $val and -x _;
# return undef;
# },
#},
});
}
# Here's the place for target-specific variables
sub targetvars {
my $class = shift;
return $class->_makevars($class->SUPER::targetvars, {
#weight => { _doc => "The weight of the pingpong ball in grams",
# _example => 15
#},
});
}
sub ProbeDesc($){
my $self = shift;
return "pingpong points";
}
# this is where the actual stuff happens
# you can access the probe-specific variables
# via the $self->{properties} hash and the
# target-specific variables via $target->{vars}
# If you based your class on 'Smokeping::probes::base',
# you'd have to provide a "ping" method instead
# of "pingone"
sub pingone ($){
my $self = shift;
my $target = shift;
# my $binary = $self->{properties}{binary};
# my $weight = $target->{vars}{weight}
# my $count = $self->pings($target); # the number of pings for this targets
# ping one target
# execute a command and parse its output
# you should return a sorted array of the measured latency times
# it could go something like this:
my @times;
#for (1..$count) {
# open(P, "$cmd 2>&1 |") or croak("fork: $!");
# while (<P>) {
# /time: (\d+\.\d+)/ and push @times, $1;
# }
# close P;
#}
return @times;
}
# That's all, folks!
1;

View File

@@ -0,0 +1,82 @@
package Smokeping::sorters::Loss;
=head1 NAME
Smokeping::sorters::Loss - Order the target charts by loss
=head1 OVERVIEW
Find the charts with the highest loss.
=head1 DESCRIPTION
Call the sorter in the charts section of the config file
+ charts
menu = Charts
title = The most interesting destinations
++ loss
sorter = Loss(entries=>10)
title = The Losers
menu = Loss
format = Packets Lost %f
=head1 COPYRIGHT
Copyright (c) 2007 by OETIKER+PARTNER AG. All rights 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 <tobi@oetiker.ch>
=cut
use strict;
use base qw(Smokeping::sorters::base);
use vars qw($VERSION);
$VERSION = 1.0;
use Carp;
# how many values does the matcher need to do it's magic
sub new(@) {
my $class = shift;
my $rules = {
entries => '\d+'
};
my $self = $class->SUPER::new( $rules, @_ );
return $self;
}
sub Desc ($) {
return "The Median sorter sorts the targets by Median RTT.";
}
sub CalcValue($) {
my $self = shift;
my $info = shift;
# $info = { uptime => w,
# loss => x,
# median => y,
# alert => z, (0/1)
# pings => [qw(a b c d)]
#
return $info->{loss} ? $info->{loss} : -1;
}

View File

@@ -0,0 +1,81 @@
package Smokeping::sorters::Max;
=head1 NAME
Smokeping::sorters::Max - Order the target charts by Max RTT
=head1 OVERVIEW
Find the charts with the highest round trip time.
=head1 DESCRIPTION
Call the sorter in the charts section of the config file
+ charts
menu = Charts
title = The most interesting destinations
++ max
sorter = Max(entries=>10)
title = Sorted by Max Roundtrip Time
menu = by Max
format = Max Roundtrip Time %f seconds
=head1 COPYRIGHT
Copyright (c) 2007 by OETIKER+PARTNER AG. All rights 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 <tobi@oetiker.ch>
=cut
use strict;
use base qw(Smokeping::sorters::base);
use vars qw($VERSION);
$VERSION = 1.0;
use Carp;
sub new(@) {
my $class = shift;
my $rules = {
entries => '\d+'
};
my $self = $class->SUPER::new( $rules, @_ );
return $self;
}
sub Desc ($) {
return "The Max sorter sorts the targets by Max RTT.";
}
sub CalcValue($) {
my $self = shift;
my $info = shift;
# $info = { uptime => w,
# loss => x,
# median => y,
# alert => z, (0/1)
# pings => [qw(a b c d)]
#
my $max = (sort { $b <=> $a } grep { defined $_ } @{$info->{pings}})[0];
return $max ? $max : -1;
}

View File

@@ -0,0 +1,83 @@
package Smokeping::sorters::Median;
=head1 NAME
Smokeping::sorters::Median - Order the target charts by Median RTT
=head1 OVERVIEW
Find the charts with the highest Median round trip time.
=head1 DESCRIPTION
Call the sorter in the charts section of the config file
+ charts
menu = Charts
title = The most interesting destinations
++ median
sorter = Median(entries=>10)
title = Top Median round trip time
menu = Median RTT
format = Median round trip time %f seconds
=head1 COPYRIGHT
Copyright (c) 2007 by OETIKER+PARTNER AG. All rights 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 <tobi@oetiker.ch>
=cut
use strict;
use base qw(Smokeping::sorters::base);
use vars qw($VERSION);
$VERSION = 1.0;
use Carp;
# how many values does the matcher need to do it's magic
sub new(@) {
my $class = shift;
my $rules = {
entries => '\d+'
};
my $self = $class->SUPER::new( $rules, @_ );
return $self;
}
sub Desc ($) {
return "The Median sorter sorts the targets by Median RTT.";
}
sub CalcValue($) {
my $self = shift;
my $info = shift;
# $info = { uptime => w,
# loss => x,
# median => y,
# alert => z, (0/1)
# pings => [qw(a b c d)]
#
return $info->{median} ? $info->{median} : -1;
}

View File

@@ -0,0 +1,92 @@
package Smokeping::sorters::StdDev;
=head1 NAME
Smokeping::sorters::StdDev - Order the target charts by StdDev
=head1 OVERVIEW
Find the charts with the highest standard deviation among the Pings sent to
a single target. The more smoke - higher the standard deviation.
=head1 DESCRIPTION
Call the sorter in the charts section of the config file
+ charts
menu = Charts
title = The most interesting destinations
++ stddev
sorter = StdDev(entries=>4)
title = Top StdDev
menu = Std Deviation
format = Standard Deviation %f
=head1 COPYRIGHT
Copyright (c) 2007 by OETIKER+PARTNER AG. All rights 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 <tobi@oetiker.ch>
=cut
use strict;
use base qw(Smokeping::sorters::base);
use vars qw($VERSION);
$VERSION = 1.0;
use Carp;
# how many values does the matcher need to do it's magic
sub new(@) {
my $class = shift;
my $rules = {
entries => '\d+'
};
my $self = $class->SUPER::new( $rules, @_ );
return $self;
}
sub Desc ($) {
return "The Standard Deviation sorter sorts the targets by Standard Deviation.";
}
sub CalcValue($) {
my $self = shift;
my $info = shift;
# $info = { uptime => w,
# loss => x,
# median => y,
# alert => z, (0/1)
# pings => [qw(a b c d)]
#
my $avg = 0;
my $cnt = 0;
my @values = grep { defined $_ } @{$info->{pings}};
for (@values){ $avg += $_; $cnt++};
return -1 if $cnt == 0;
$avg = $avg / $cnt;
my $dev = 0;
for (@values){ $dev += ($_ - $avg)**2};
$dev = sqrt($dev / $cnt);
return $dev;
}

View File

@@ -0,0 +1,149 @@
package Smokeping::sorters::base;
=head1 NAME
Smokeping::sorters::base - Base Class for implementing SmokePing Sorters
=head1 OVERVIEW
Sorters are at the core of the SmokePing Charts feature, where the most
interesting graphs are presented on a single page. The Sorter decides which
graphs are considered interesting.
Every sorter must inherit from the base class and provide it's own
methods for the 'business' logic.
In order to maintain a decent performance the sorters activity is split into
two parts.
The first part is active while the smokeping daemon gathers its data.
Whenever data is received, the sorter is called to calculate a 'value' for
the present data. On every 'query round' this information is stored in the
sorter store directory. Each smokeping process stores it's own information.
Since smokeping can run in multiple instances at the same time, the data may
be split over several files
The second part of the sorter is called from smokeping.cgi. It loads all the
information from the sorter store and integrates it into a single 'tree'. It
then calls each sorter with the pre-calculated data to get it sorted and to
and to select the interesting information.
=head1 DESCRIPTION
Every sorter must provide the following methods:
=cut
use vars qw($VERSION);
use Carp;
$VERSION = 1.0;
use strict;
=head2 new
The new method expects hash elements as an argument
eg new({x=>'\d+',y=>'\d+'},x=>1,y=>2). The first part is
a syntax rule for the arguments it should expect and the second part
are the arguments itself. The first part will be supplied
by the child class as it calls the parent method.
=cut
sub new(@)
{
my $this = shift;
my $class = ref($this) || $this;
my $rules = shift;
my $self = { param => { @_ } };
foreach my $key (keys %{$self->{param}}){
my $regex = $rules->{$key};
croak "key '$key' is not known by this sorter" unless defined $rules->{$key};
croak "key '$key' contains invalid data: '$self->{param}{$key}'" unless $self->{param}{$key} =~ m/^$regex$/;
}
bless $self, $class;
return $self;
}
=head2 Desc
Simply return the description of the function. This method must
be overwritten by a children of the base class.
=cut
sub Desc ($) {
croak "Sorter::Desc must be overridden by the subclass";
}
=head2 SortTree
Returns an array of 'targets'. It is up to the sorter to decide how many
entries the list should contain. If the list is empty, the whole entry will
be suppressed in the webfrontend.
The method gets access to all the targets in the system, together with the
last data set acquired for each target.
=cut
sub SortTree($$) {
my $self = shift;
my $target = shift @{$self->{targets}};
my $cache = shift;
my $entries = $self->{param}{entries} || 3;
my $sorted = [
map { $entries-- > 0 ? { open => [ split '/', $_ ], value => $cache->{$_} } : () }
sort { $cache->{$b} <=> $cache->{$a} } keys %$cache ];
return $sorted;
}
=head2 CalcValues
Figure out the current sorting value using te following input.
$info = { uptime => w,
loss => x,
median => y,
alert => z, # (0/1)
pings => [qw(a b c d)] }
The output can have any structure you want. It will be returned to the
sorter method for further processing.
=cut
sub CalcValue($) {
my $self = shift;
my $info = shift;
croak "CalcValue must be overridden by the subclass";
return ( { any=>'structure' } );
}
=head1 COPYRIGHT
Copyright (c) 2007 by OETIKER+PARTNER AG. All rights 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 <tobi@oetiker.ch>
=cut