#!/usr/bin/env perl package Munin::Plugin::Multiping::Async; use 5.10.0; use MooseX::POE; use MooseX::POE::SweetArgs qw(event); use POE::Quickie; use Munin::Plugin; use Storable; use Digest; =head1 NAME multiping_async - Like the multiping plugin but runs asynchronously =head1 SYNOPSIS munin-run multiping_async =head1 CONFIGURATION The following environment variables are used: host - Whitespace-separated list of hosts to ping times - How many times to ping the hosts, 3 by default timeout - The ping timeout (ping -W), 1 by default (ignored on Solaris) title - The graph_title to use for the munin RRD graph category - What category the graph should be in, network by default Configuration example, ping all the Linode clusters: # An optional custom category [multiping_async_*] env.category ping [multiping_async_linode] # From http://www.linode.com/speedtest/ env.title Ping times to all the Linode clusters env.host london1.linode.com newark1.linode.com atlanta1.linode.com dallas1.linode.com fremont1.linode.com =head1 DESCRIPTION Like the L plugin except that it runs L asynchronously with POE, and you can add/remove hosts later on without screwing up your RRD files (multiping reports statistics based on the order of hosts in C). This plugin used to use L but I switched away from it due to having odd timing issues with it, and it had to run as root. This plugin requires the L and L modules from CPAN. It has been tested with the Linux, FreeBSD and Solaris L implementations. =head1 AUTHOR Ævar Arnfjörð Bjarmason =head1 LICENSE This program is in the public domain. =head1 MAGIC MARKERS #%# family=manual =cut has graph_title => ( isa => 'Str', is => 'ro', default => $ENV{title} // 'Ping times', documentation => 'The munin graph_title', ); has hosts => ( isa => 'ArrayRef', is => 'ro', auto_deref => 1, default => sub { my $host = $ENV{host} // ''; return [ split /\s+/, $host ] }, documentation => "Hosts we're going to ping", ); has times => ( isa => 'Int', is => 'ro', default => $ENV{times} // 3, documentation => "How many times we ping each host (ping -c)", ); has timeout => ( isa => 'Int', is => 'ro', default => $ENV{timeout} // 1, documentation => "How long until ping timeouts (ping -W)", ); has category => ( isa => 'Str', is => 'ro', default => $ENV{category} // 'network', documentation => "What munin category we should appear in", ); has should_config => ( isa => 'Bool', is => 'ro', default => sub { defined $ARGV[0] and $ARGV[0] eq "config" }, documentation => 'Spew out config section?', ); has response => ( isa => 'HashRef', is => 'ro', auto_deref => 0, default => sub { +{} }, documentation => 'To store ping responses', ); has statefile => ( isa => 'Str', is => 'ro', default => $ENV{MUNIN_STATEFILE}, documentation => 'Where we store state between invocations', ); sub START { my ($self) = @_; die "You must supply some hosts" unless @{ $self->hosts } > 0; if ($self->should_config) { $self->print_config; return; } for my $host ($self->hosts) { POE::Quickie->new->run( Program => [ $self->ping_arguments($host) ], StdoutEvent => 'stdout', ExitEvent => 'exit', Context => $host, ); } } sub ping_arguments { my ($self, $host) = @_; given ($^O) { when ('solaris') { return ('ping', '-s', $host, '64', $self->times); } default { # Linux and FreeBSD return ('ping', '-c', $self->times, '-W', $self->timeout, => $host); } } } event stdout => sub { my ($self, $output, undef, $context) = @_; given ($output) { my $noslash = qr{[^/]+}; # Linux output: rtt min/avg/max/mdev = 7.218/7.255/7.293/0.030 ms # BSD output : round-trip min/avg/max/stddev = 34.935/35.665/36.684/0.743 ms # Solaris : round-trip (ms) min/avg/max/stddev = 5.82/5.95/6.01/0.11 when (m[= (?$noslash)/(?$noslash)/(?$noslash)/]) { $self->response->{ $context } = $+{avg}; } } }; event exit => sub { my ($self, $code, $x, $context) = @_; given ($code) { when (0) { die "Got no response from $context" unless exists $self->response->{ $context }; $self->yield( print_host => $context => $self->response->{ $context } ); } default { # Host down, probably $self->yield( print_host => $context => 0 ); } } return; }; sub STOP { my ($self) = @_; if (not $self->should_config and my $file = $self->statefile) { my $res = $self->response; my $ret = store($res, $file); # use Data::Dumper; # say Dumper { gonna_store => $res, ret => $ret, file => $file }; } } sub print_config { my ($self) = @_; my $title = $self->graph_title; my $times = $self->times; my $category = $self->category; print <sorted_hosts) { my $fieldname = $self->fieldname($host); print <hosts; my $state = $self->statefile; given ($self->statefile) { when (-e and -r) { my $last_res = retrieve($_); my @sorted = sort { $last_res->{$b} <=> $last_res->{$a} } keys %$last_res; if ($last_res and @hosts == @sorted) { return @sorted; } } } return @hosts; } event print_host => sub { my ($self, $context, $time) = @_; my $fieldname = $self->fieldname($context); my $value = sprintf "%6.6f", $time; say "$fieldname.value $value"; }; sub fieldname { my ($self, $name) = @_; my $sha1 = substr Digest->new("SHA-1")->add($name)->hexdigest, 0, 10; return clean_fieldname($name) . '_' . $sha1; } no MooseX::POE; Munin::Plugin::Multiping::Async->new; POE::Kernel->run;