#! /usr/bin/perl ######################################################################## # Copyright (c) 2012, Adrien Urban # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the # distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # ######################################################################## # # # WARNING WARNING WARNING WARNING WARNING WARNING # # # # This plugin does not work properly with multiple master # # # ######################################################################## # # multigraph, supersampling, detailed interfaces statistics # # require: ifconfig # linux only for now. Would need to implement way to gather the same # data for other ifconfig usage and output. # require: Time::HiRes # # ENV (default): # MUNIN_PLUGSTATE - pid and cache files gets there # # ENV (user defined): # MUNIN_UPDATERATE - rate at which to update (default: 1s) # MUNIN_CACHEFLUSH_RATE - flush data every N batch (default: 1) # MUNIN_IFCONFIG - path for ifconfig (default /sbin/ifconfig) # # MUNIN_IF_INCLUDE - list of interfaces to graph (all by default) # MUNIN_IF_EXCLUDE - exclude all of those interfaces (none by default) # MUNIN_GRAPH_BYTES - do graph bytes per seconds (default: yes) # MUNIN_GRAPH_PACKETS - do graph packets per seconds (default: yes) # MUNIN_GRAPH_ERRORS - do graph errors (default: yes) # # Parent graphs: none # child graphs: per interface - bytes, packets, errors # interfaces/XXX/{bw,pkt,err} # # Known bugs: # # Multi-Master # If there are many masters, the data is only sent once. Each master will # only have part of the data. # # Everlasting # The daemon is launched on first config/fetch. A touch of the pidfile is # done on every following config/fetch. The daemon should check if the # pidfile is recent (configurable) enough, and stop itself if not. # # Graph Order # There is currently (2.0.6) noway to order childgraphs. # # RRD file # The master currently (2.0.6) generate rrd file for aggregate values, and # complains that no data is provided for them (but the graph still works # fine) # #%# family=auto #%# capabilities=autoconf use strict; use warnings; use Time::HiRes; use IO::Handle; my $plugin = $0; $plugin =~ s/.*\///; # quick failsafe if (!defined $ENV{MUNIN_PLUGSTATE}) { die "This plugin should be run via munin. Try munin-run $plugin\n"; } ######################################################################## # If you want to change something, it's probably doable here # sub pidfile() { "$ENV{MUNIN_PLUGSTATE}/munin.$plugin.pid" } sub cachefile() { "$ENV{MUNIN_PLUGSTATE}/munin.$plugin.cache" } sub graph_name() { "interfaces" } #sub graph_title() { "interfaces" } #sub graph_title_all() { "Overall CPU usage" } #sub graph_title_n($) { "CPU#" . shift . " usage" } sub acquire_name() { "<$plugin> collecting information" } # Default update rate. Can be changed by configuration. my $update_rate = 1; # default flush interval. Can be changed by configuration. my $flush_interval = 1; # default ifconfig command. Can be changed by configuration my $ifconfig = '/sbin/ifconfig'; ######################################################################## # if you need to change something after that line, It should probably be # changed to be configurable above it. # if (defined $ENV{MUNIN_UPDATERATE}) { if ($ENV{MUNIN_UPDATERATE} =~ /^[1-9][0-9]*$/) { $update_rate = int($ENV{MUNIN_UPDATERATE}); } else { print STDERR "Invalid update_rate: $ENV{MUNIN_UPDATERATE}"; } } if (defined $ENV{MUNIN_CACHEFLUSH_RATE}) { if ($ENV{MUNIN_CACHEFLUSH_RATE} =~ /^[0-9]+$/) { $flush_interval = int($ENV{MUNIN_CACHEFLUSH_RATE}); } else { print STDERR "Invalid flush rate: $ENV{MUNIN_CACHEFLUSH_RATE}"; } } if (defined $ENV{MUNIN_IFCONFIG}) { if (-f $ENV{MUNIN_IFCONFIG}) { print STDERR "MUNIN_IFCONFIG: file not found: $ENV{MUNIN_IFCONFIG}"; } else { $ifconfig = defined $ENV{MUNIN_IFCONFIG}; } } my $include_list = undef; if (defined $ENV{MUNIN_IF_INCLUDE}) { $include_list = [ split(/[[:space:]]+/, $ENV{MUNIN_IF_INCLUDE}) ]; if (0 == scalar (@$include_list)) { $include_list = undef; } elsif ('' eq $include_list->[0]) { shift @$include_list; } } my $exclude_list = undef; if (defined $ENV{MUNIN_IF_EXCLUDE}) { $exclude_list = [ split(/[[:space:]]+/, $ENV{MUNIN_IF_EXCLUDE}) ]; if (0 == scalar (@$exclude_list)) { $exclude_list = undef; } elsif ('' eq $exclude_list->[0]) { shift @$exclude_list; } } sub configbool($) { my $str = shift; if ($str =~ /^(y(es)?|1|t(rue)?)$/i) { return 1; } if ($str =~ /^(no?|0|f(alse)?)$/i) { return 0; } print STDERR "$str: unrecognized bool\n"; return 1; } my $should_graph = { 'bytes' => 1, 'packets' => 1, 'errors' => 1, }; if (defined $ENV{MUNIN_GRAPH_BYTES}) { $should_graph->{'bytes'} = configbool($ENV{MUNIN_GRAPH_BYTES}); } if (defined $ENV{MUNIN_GRAPH_PACKETS}) { $should_graph->{'packets'} = configbool($ENV{MUNIN_GRAPH_PACKETS}); } if (defined $ENV{MUNIN_GRAPH_ERRORS}) { $should_graph->{'errors'} = configbool($ENV{MUNIN_GRAPH_ERRORS}); } unless ($should_graph->{bytes} or $should_graph->{packets} or $should_graph->{errors}) { die "Nothing to graph!"; } ######################################################################## # Base functions, specific to what we really try to do here. # sub included_interface($) { my $if = shift; if (defined $exclude_list) { foreach my $ifl (@$exclude_list) { return 0 if ($if =~ /^($ifl)$/); } } if (defined $include_list) { foreach my $ifl (@$include_list) { return 1 if ($if =~ /^($ifl)$/); } return 0; } return 1; } sub if_to_name($) { my $if = shift; $if =~ s/[^A-Za-z0-9]/_/g; return $if; } sub get_data() { open IFCONFIG, "-|", $ifconfig or die "open: $ifconfig|: $!\n"; my $data = {}; my $current_if = undef; while () { if (/^([^[:space:]]+)/) { $current_if = $1; if (!included_interface($current_if)) { $current_if = undef; next } $data->{$current_if} = {}; next; # nothing else on that line } next if (!defined $current_if); if (/RX packets:([0-9]+) errors:([0-9]+) dropped:([0-9]+) overruns:([0-9]+) frame:([0-9]+)/) { $data->{$current_if}{'rx_pkt'} = $1; $data->{$current_if}{'rx_err'} = $2; $data->{$current_if}{'rx_drp'} = $3; $data->{$current_if}{'rx_ovr'} = $4; $data->{$current_if}{'rx_frm'} = $5; next; } if (/TX packets:([0-9]+) errors:([0-9]+) dropped:([0-9]+) overruns:([0-9]+) carrier:([0-9]+)/) { $data->{$current_if}{'tx_pkt'} = $1; $data->{$current_if}{'tx_err'} = $2; $data->{$current_if}{'tx_drp'} = $3; $data->{$current_if}{'tx_ovr'} = $4; $data->{$current_if}{'tx_car'} = $5; next; } if (/RX bytes:([0-9]+) \([^)]*\) TX bytes:([0-9]+) /) { $data->{$current_if}{'rx_byt'} = $1; $data->{$current_if}{'tx_byt'} = $2; } } close IFCONFIG; return $data; } # values names, from a data line sub get_data_names($) { my $line = shift; my $name = $line->[0]; my $count = scalar(@$line) - 2; # 2: name, and timestamp if ($name =~ /\.(bps|pkt)$/ and 2 == $count) { return [ 'rx', 'tx' ]; } if ($name =~ /\.err$/ and 8 == $count) { return [ 'rxerr', 'txerr', 'rxdrp', 'txdrp', 'rxovr', 'txovr', 'rxfrm', 'txcar', ]; } # no idea what it is ? corrupted data return undef; } sub collect_info_once($$) { my $fh = shift; my $now = shift; my $data = get_data(); foreach my $if (keys %$data) { my $name = if_to_name($if); my $d = $data->{$if}; if ($should_graph->{'bytes'}) { print $fh <{'rx_byt'} $d->{'tx_byt'} EOF #$name.byt $now rx $d->{'rx_byt'} #$name.byt $now tx $d->{'tx_byt'} } if ($should_graph->{'packets'}) { print $fh <{'rx_pkt'} $d->{'tx_pkt'} EOF #$name.pkt $now rx $d->{'rx_pkt'} #$name.pkt $now tx $d->{'tx_pkt'} } if ($should_graph->{'errors'}) { print $fh <{'rx_err'} $d->{'tx_err'} $d->{'rx_drp'} $d->{'tx_drp'} $d->{'rx_ovr'} $d->{'tx_ovr'} $d->{'rx_frm'} $d->{'tx_car'} EOF #$name.err $now rxerr $d->{'rx_err'} #$name.err $now txerr $d->{'tx_err'} #$name.err $now rxdrp $d->{'rx_drp'} #$name.err $now txdrp $d->{'tx_drp'} #$name.err $now rxovr $d->{'rx_ovr'} #$name.err $now txovr $d->{'tx_ovr'} #$name.err $now rxfrm $d->{'rx_frm'} #$name.err $now txcar $d->{'tx_car'} } } } sub show_config() { my $data = get_data(); my $graph_order = "graph_order"; foreach my $if (sort keys %$data) { my $name = if_to_name($if); $graph_order .= " ${name}_bps=${name}.bps.tx"; } print <{'bytes'}) { print <{'packets'}) { print <{'errors'}) { print <; close FILE; chomp $pid; } if ($pid) { # does not exist ? kill it if (kill 0, $pid) { return 1; } } unlink(pidfile()); } return 0; } # FIXME: should also trap kill sigint and sigterm # FIXME: check pidfile got touched recently sub collect_loop() { $0 = acquire_name(); $0 = "<$plugin> collecting information"; # write our pid open PIDFILE, '>', pidfile() or die "open: @{[ pidfile() ]}: $!\n"; print PIDFILE $$, "\n"; close PIDFILE; # open cache my $fh_cache; open $fh_cache, ">>", cachefile() or die "open: @{[ cachefile() ]}: $!\n"; my @tick = Time::HiRes::gettimeofday(); my $flush_count = 0; while (1) { collect_info_once($fh_cache, $tick[0]); if ($flush_interval) { if ($flush_interval == ++$flush_count) { $fh_cache->flush(); $flush_count = 0; } } my @now = Time::HiRes::gettimeofday(); # when should the next tick be ? $tick[0] += $update_rate; # how long until next tick ? my $diff = ($tick[0] - $now[0]) * 1000000 + $tick[1] - $now[1]; if ($diff <= 0) { # next tick already passed ? damn! @tick = @now; } else { # sleep what remains Time::HiRes::usleep($diff); } } unlink(pidfile()); unlink(cachefile()); } # launch daemon if not running # notify the daemon we still need it (touch its pid) sub daemon_alive() { if (check_running()) { my $atime; my $mtime; $atime = $mtime = time; utime $atime, $mtime, pidfile(); } else { if (0 == fork()) { close(STDIN); close(STDOUT); close(STDERR); open STDIN, "<", "/dev/null"; open STDOUT, ">", "/dev/null"; open STDERR, ">", "/dev/null"; collect_loop(); exit(0); } } } sub run_autoconf() { if (check_req()) { print "yes\n"; } else { print "no\n"; } } sub run_config() { daemon_alive(); show_config(); } sub fetch_showline($) { my $line = shift; my $names = get_data_names($line); # don't display anything if we don't like what it is return unless (defined $names); my $graph = shift @$line; my $time = shift @$line; foreach my $value (@$line) { my $name = shift @$names; print <) { chomp; my $field = []; @$field = split(/ /); if (not defined $data->{$field->[0]}) { $data->{$field->[0]} = []; } push @{$data->{$field->[0]}}, $field; } # finished reading ? truncate it right away truncate CACHE, 0; close CACHE; foreach my $graph (keys %$data) { print <{$graph}}) { fetch_showline($line); } } } } my $cmd = 'fetch'; if (defined $ARGV[0]) { $cmd = $ARGV[0]; } if ('fetch' eq $cmd) { run_fetch(); } elsif ('config' eq $cmd) { run_config(); } elsif ('autoconf' eq $cmd) { run_autoconf(); } elsif ('daemon' eq $cmd) { run_daemon(); } else { print STDERR <