#!/usr/bin/perl # This script is used to feed sensor information from Allnet-Devices like e.g. # Allnet 4000 or 4500 into munin. It uses the XML-Interface of the devices to # fetch data. # # written by Michael Meier , License GPLv2 # # Usage: # Put this script onto some server, either on your munin-server or onto some # other server that can reach the actual device. Then symlink to it from the # munin-node plugin dir (usually /etc/munin/plugins). The naming of the symlink # is a critical part of the configuration, it needs to be: # SOMETHING_HOSTNAME_TYPE, e.g. allnet_monitordevice42.yourdomain.com_temp # SOMETHING really is just a random name containing no special chars, use # whatever you like. # HOSTNAME is the hostname of the Allnet-Device. This is also the hostname # under which the sensors will show up in munin. # TYPE is the type of sensors you're interested in, valid values are: # temp, hum, amps # If you want to monitor more than one type of sensor, create more than one # symlink. # The plugin will automatically feed all sensors of the selected type to munin, # under the names that you set in the allnets config. Take care with special # characters, in particular Umlauts and the such - munin will probably not like # those in names. # # Note that the format of the XML output these devices deliver varies vastly # between different firmware versions. This script currently understands 3 # completely different outputs, but there is no guarantee that's all the # variations that exist. If the script doesn't understand the output of your # device, set the beextratolerant parameter. You might also want to try a # different firmware version. # # The behaviour of the script can be influenced by environment variables. The # variables are (note this is case sensitive!): # username Username for basic HTTP auth (if your device is password protected) # password Password for basic HTTP auth (if your device is password protected) # beextratolerant This needs to be set to 1 for some extremely old/stripped # down devices. Note that this relaxes sanity checks and # will lead to nonsense-sensors showing up on devices that # do not require this. # DUMP set to 1 to enable some debug output (only outside of munin!) # internalsensorname.warning The warning-threshold for the sensor reported # to munin. You need to replace # "internalsensorname" with the munin-internal # name of the sensor, usually sensor+somenumber. # Can be seen in the webinterface. # internalsensorname.critical Same as above, but for the critical-threshold. # # As an example, you could put the following file into /etc/munin/plugin-conf.d: # [allnet_monitordevice42.yourdomain.com_*] # env.username horst # env.password topsecret # env.sensor14.warning 10.0 # env.sensor14.critical 15.0 # One tuneable: # Timeout for requests. my $timeout = 3; # the LWP default of 180 secs would be way too long # ----------------------------------------------------------------------------- # No need to change those, can be overridden from commandline. my $hostname = 'localhost'; my $valtype = 'temp'; # or humidity or amps # These are tried one after another. my @xmlpaths = ('/xml/sensordata.xml', '/xml'); use LWP::UserAgent; use XML::Simple; # AuthAgent is simply LWP::UserAgent with a reimplemented version of the # method basic auth. Theoretically/according to the documentation, the base # implementation should be able to do everything that is needed as well, but # it seems to broken and "the internet" suggests it has been for a long time. { package AuthAgent; use base 'LWP::UserAgent'; sub get_basic_credentials { if (defined($ENV{'username'}) && defined($ENV{'password'})) { return $ENV{'username'}, $ENV{'password'}; } else { return 'admin', 'password'; } } } # Par. 0: URL # Returns: The contents of the website sub geturl($) { my $url = shift(); my $ua = AuthAgent->new(); $ua->agent($0); $ua->timeout($timeout); # Create a request my $req = HTTP::Request->new(GET => $url); # Pass request to the user agent and get a response back my $res = $ua->request($req); # Check the outcome of the response unless ($res->is_success()) { #print("ERROR getting data from $url\n"); return undef; } return $res->content(); } if ((@ARGV > 0) && ($ARGV[0] eq "autoconf")) { print("No\n"); exit(0); } my $progname = $0; if ($progname =~ m/[-_]temp$/) { $valtype = 'temp'; } elsif ($progname =~ m/[-_]temperature$/) { $valtype = 'temp'; } elsif ($progname =~ m/[-_]hum$/) { $valtype = 'humidity'; } elsif ($progname =~ m/[-_]humidity$/) { $valtype = 'humidity'; } elsif ($progname =~ m/[-_]amps$/) { $valtype = 'amps'; } elsif ($progname =~ m/[-_]ampere$/) { $valtype = 'amps'; } if ($progname =~ m/.+_(.+?)_.+/) { $hostname = $1; } my $sensorxml; foreach $xp (@xmlpaths) { $sensorxml = geturl('http://' . $hostname . $xp); if (defined($sensorxml)) { last; } } unless (defined($sensorxml)) { print("# Sorry, Failed to fetch data."); exit(1); } # VERY old firmware versions have HTML crap around the actual XML. $sensorxml =~ s!.*(.*).*!$1!sg; if (defined($ENV{'DUMP'}) && ($ENV{'DUMP'} eq '1')) { print($sensorxml . "\n"); } if ((@ARGV > 0) && ($ARGV[0] eq "config")) { if ($valtype eq 'humidity') { print("graph_title Humidity sensors\n"); print("graph_args --lower-limit 0 --upper-limit 100.0\n"); print("graph_vlabel percent\n"); } elsif ($valtype eq 'temp') { print("graph_title Temperature sensors\n"); print("graph_vlabel degC\n"); } elsif ($valtype eq 'amps') { print("graph_title Electric current sensors\n"); print("graph_args --lower-limit 0\n"); print("graph_vlabel Ampere\n"); } print("graph_category sensors\n"); print("host_name $hostname\n"); } my $sensordata = XMLin($sensorxml, KeyAttr => { }, ForceArray => [ 'data' ] ); my $beextratolerant = 0; if (defined($ENV{'beextratolerant'})) { if (($ENV{'beextratolerant'} =~ m/^y/) || ($ENV{'beextratolerant'} eq '1')) { $beextratolerant = 1; } } #print($sensordata->[0]); foreach $k (keys($sensordata)) { if ($k =~ m/^n(\d+)$/) { # Special handling: Could be output from the OLD XML interface. my $nr = $1; if (defined($sensordata->{'s'.$nr}) && defined($sensordata->{'t'.$nr}) && defined($sensordata->{'min'.$nr}) && defined($sensordata->{'max'.$nr})) { # OK, all values available, really is from the old XML interface. if ($sensordata->{'s'.$nr} eq '0') { next; } # 0 means no sensor. # OK, lets map the sensortype. my $st = 'temp'; if ($sensordata->{'s'.$nr} eq '65') { $st = 'humidity'; } if ($sensordata->{'s'.$nr} eq '101') { # potential FIXME: 101 is actually "water/contact sensor", but since it # returns 0.0 and 100.0 as values, we can treat it just like humidity # for simplicity. $st = 'humidity'; } if ($valtype ne $st) { next; } # these aren't the droids you're looking for if ((@ARGV > 0) && ($ARGV[0] eq "config")) { print("sensor${nr}.label " . $sensordata->{'n'.$nr} . "\n"); print("sensor${nr}.type GAUGE\n"); if (defined($ENV{"sensor${nr}.warning"})) { printf("sensor%s.warning %s\n", $nr, $ENV{"sensor${nr}.warning"}); } if (defined($ENV{"sensor${nr}.critical"})) { printf("sensor%s.critical %s\n", $nr, $ENV{"sensor${nr}.critical"}); } } else { print("sensor${nr}.value " . $sensordata->{'t'.$nr}. "\n"); } } # Fall through - probably wasn't the old XML anyways. } my $onesens = $sensordata->{$k}; unless (defined($onesens->{'value_float'}) && defined($onesens->{'name'}) && defined($onesens->{'min_abs_float'}) && defined($onesens->{'max_abs_float'}) && defined($onesens->{'unit'})) { # Not all values available -> no sane sensor data (or the "system" block in the XML) unless ($beextratolerant) { next; } # Maybe yet another firmware version that does not deliver all of these values, # so lets check for the absolute MINIMUM set. unless (defined($onesens->{'value_float'}) && defined($onesens->{'name'})) { next; # Minimum set not available either. } if ($onesens->{'value_float'} < -20000.0) { next; } # Invalid value # OK, so fill in the blanks unless (defined($onesens->{'unit'})) { $onesens->{'unit'} = 'C'; } unless (defined($onesens->{'min_abs_float'})) { $onesens->{'min_abs_float'} = $onesens->{'value_float'} - 0.01; } unless (defined($onesens->{'max_abs_float'})) { $onesens->{'max_abs_float'} = $onesens->{'value_float'} + 0.01; } } if (($onesens->{'min_abs_float'} == 0.0) && ($onesens->{'max_abs_float'} == 0.0) && ($onesens->{'value_float'} == 0.0)) { next; # If the sensor never showed anything but 0.0, we do not care about it. } if ($onesens->{'unit'} eq 'A AC') { $onesens->{'unit'} = 'A'; } if ($onesens->{'unit'} =~ m/C$/) { $onesens->{'unit'} = 'C'; # Remove WTF8-Fuckup } if (($valtype eq 'temp') && ($onesens->{'unit'} ne 'C')) { next; } if (($valtype eq 'humidity') && ($onesens->{'unit'} ne '%')) { next; } if (($valtype eq 'amps') && ($onesens->{'unit'} ne 'A')) { next; } if ((@ARGV > 0) && ($ARGV[0] eq "config")) { print("${k}.label " . $onesens->{'name'} . "\n"); #print("${k}.info " . $onesens->{'name'} . "\n"); print("${k}.type GAUGE\n"); if (defined($ENV{"${k}.warning"})) { printf("%s.warning %s\n", $k, $ENV{"${k}.warning"}); } if (defined($ENV{"${k}.critical"})) { printf("%s.critical %s\n", $k, $ENV{"${k}.critical"}); } } else { if ($onesens->{'value_float'} < -20000.0) { # Invalid readings return -20480.0 print("${k}.value U\n"); } else { print("${k}.value " . $onesens->{'value_float'} . "\n"); } } }