#! /usr/bin/perl -w =head1 NAME nutups2_ - Plugin to monitor UPSes managed by NUT =head1 CONFIGURATION Generally none needed. If you have installed NUT at a non-standard location, then you can specify its location like: [nutups2_*] env.upsc /some/location/bin/upsc =head1 WARNING AND CRITICAL SETTINGS If upsc reports 'high' and 'low' values for some attribute, those will used as the critical range. Otherwise the following environment variables can be used to set the defaults for all fields: env.warning env.critical You can also control individual fields like: env.input_L1.warning env.output.critical =head1 MAGIC MARKERS #%# family=auto #%# capabilities=autoconf suggest =head1 FEATURES The plugin supports reporting battery charge, UPS load, input/output frequencies/currents/voltages, apparent and real power output, humidity and temperature readings. Note however that different UPS models report different levels of detail; the plugin reports whatever information the NUT UPS driver (and in turn the UPS itself) provides. Although the 'suggest' command will only offer UPSes for which the local host is the master, you can also monitor remote UPSes if you include the host name in the symlink, like: nutups2_@_frequency etc. =head1 AUTHOR Gábor Gombás =head1 LICENSE GPLv2 or later =cut use strict; use Munin::Plugin; use Carp; my $UPSC = $ENV{'upsc'} || 'upsc'; # For the 'filter' field, the first sub-match should contain the name to # display, and the second sub-match should indicate if it is a nominal # value instead of a sensor reading. my %config = ( charge => { filter => qr/^(.*)\.(?:charge|load)$/, title => 'UPS load and battery charge', args => '--base 1000 -l 0 -u 100', vlabel => '%', config => \&common_config, fetch => \&common_fetch, }, current => { filter => qr/^(.*)\.current(\.nominal)?$/, title => 'UPS current', args => '--base 1000 -l 0', vlabel => 'Amper', config => \&common_config, fetch => \&common_fetch, }, frequency => { filter => qr/^(.*)\.frequency(\.nominal)?$/, title => 'UPS frequency', args => '--base 1000 -l 0', vlabel => 'Hz', config => \&common_config, fetch => \&common_fetch, }, humidity => { filter => qr/^(.*)\.humidity$/, title => 'UPS humidity', args => '--base 1000 -l 0', vlabel => '%', config => \&common_config, fetch => \&common_fetch, }, power => { filter => qr/^(.*)\.power(\.nominal)?$/, title => 'UPS apparent power', args => '--base 1000 -l 0', vlabel => 'VA', config => \&common_config, fetch => \&common_fetch, }, realpower => { filter => qr/^(.*)\.realpower(\.nominal)?$/, title => 'UPS real power', args => '--base 1000 -l 0', vlabel => 'Watt', config => \&common_config, fetch => \&common_fetch, }, temperature => { filter => qr/^(.*)\.temperature$/, title => 'UPS temperature', args => '--base 1000 -l 0', vlabel => 'Celsius', config => \&common_config, fetch => \&common_fetch, }, voltage => { filter => qr/^(.*)\.voltage(\.nominal)?$/, title => 'UPS voltage', args => '--base 1000 -l 0', vlabel => 'Volt', config => \&common_config, fetch => \&common_fetch, }, ); sub read_ups_values { my $ups = shift; my @lines = `$UPSC $ups 2>/dev/null`; my $values = {}; for my $line (@lines) { chomp $line; my ($key, $value) = $line =~ m/^([^:]+):\s+(\S.*)$/; $values->{$key} = $value; } return $values; } sub graph_config { my ($func, $ups, $values) = @_; print "graph_title " . $config{$func}->{'title'} . " ($ups)\n"; print "graph_vlabel " . $config{$func}->{'vlabel'} . "\n"; print "graph_args " . $config{$func}->{'args'} . "\n"; print "graph_category sensors\n"; my @info; push @info, 'Manufacturer: "' . $values->{'ups.mfr'} . '"' if exists $values->{'ups.mfr'} and $values->{'ups.mfr'} ne 'unknown'; push @info, 'Model: "' . $values->{'ups.model'} . '"' if exists $values->{'ups.model'}; push @info, 'Serial: "' . $values->{'ups.serial'} . '"' if exists $values->{'ups.serial'}; map { s/\s+/ /g } @info; print "graph_info " . join(', ', @info) . "\n" if @info; } sub print_range_warning { my ($id, $key, $values) = @_; if (exists $values->{$key . '.minimum'}) { print $id . ".min " . $values->{$key . '.minimum'} . "\n"; } if (exists $values->{$key . '.maximum'}) { print $id . ".max " . $values->{$key . '.maximum'} . "\n"; } my $range = ''; if (exists $values->{$key . '.high'}) { $range = $values->{$key . '.high'}; } if (exists $values->{$key . '.low'}) { $range = $values->{$key . '.low'} . ':' . $range; } # print_thresholds() needs 'undef' for no range undef $range unless $range; print_thresholds($id, undef, undef, undef, $range); } # Example keys for voltages: # battery.voltage # battery.voltage.minimum # battery.voltage.maximum # battery.voltage.nominal # input.voltage # input.voltage.minimum # input.voltage.maximum # input.bypass.L1-N.voltage # input.L1-N.voltage # output.voltage # output.voltage.nominal # output.L1-N.voltage # # Replace 'voltage' with 'current' in the above list to get an example # for current keys. # # Frequency keys: # input.frequency # input.frequency.nominal # input.bypass.frequency # input.bypass.frequency.nominal # output.frequency # output.frequency.nominal # output.frequency.minimum # output.frequency.maximum sub common_config { my ($func, $ups) = @_; my $values = read_ups_values($ups); graph_config($func, $ups, $values); for my $key (sort keys %$values) { my ($field, $nominal) = $key =~ $config{$func}->{'filter'}; next unless $field; $field .= $nominal if $nominal; my $id = clean_fieldname($field); # These labels look better this way and are still short enough $field = $key if $func =~ m/(charge|temperature|humidity)/; # Beautification $field = ucfirst($field); $field =~ s/\./ /g; print $id . ".label " . $field . "\n"; print $id . ".type GAUGE\n"; # Draw nominal values a litle thinner print $id . ".draw LINE1\n" if $nominal; print_range_warning($id, $key, $values); } } sub common_fetch { my ($func, $ups) = @_; my $values = read_ups_values($ups); for my $key (sort keys %$values) { my ($field, $nominal) = $key =~ $config{$func}->{'filter'}; next unless $field; $field .= $nominal if $nominal; my $id = clean_fieldname($field); print $id . ".value " . $values->{$key} . "\n"; } } if ($ARGV[0] and $ARGV[0] eq 'autoconf') { # The former nutups_ plugin parsed upsmon.conf. But for a large UPS # that powers dozens or hundreds of machines, that would mean # monitoring the same UPS from every host it powers, which does not # make sense. Using upsc and defaulting to localhost means that # 'autoconf' will only enable the plugin on the UPS master node, where # upsd is running. my @upses = `$UPSC -l 2>/dev/null`; if ($?) { if ($? == -1) { print "no (program '$UPSC' was not found)\n"; } else { print "no (program '$UPSC -l' returned error)\n"; } exit 0; } map { chomp $_ } @upses; unless (@upses and $upses[0]) { print "no (program '$UPSC' listed no units)\n"; } else { print "yes\n"; } exit 0; } if ($ARGV[0] and $ARGV[0] eq 'suggest') { my @upses = `$UPSC -l 2>/dev/null`; for my $ups (@upses) { chomp $ups; for my $metric (keys %config) { my $values = read_ups_values($ups); my @keys = grep { +$_ =~ $config{$metric}->{'filter'} } keys(%$values); print $ups . '_' . $metric . "\n" if @keys; } } exit 0; } croak("Unknown command line arguments") if $ARGV[0] and $ARGV[0] ne 'config'; # The UPS name may contain underscores my $fn_re = join('|', keys %config); my ($ups, $func) = $0 =~ m/nutups2_(.*)_($fn_re)$/; if ($ARGV[0] and $ARGV[0] eq 'config') { $config{$func}->{'config'}($func, $ups); } else { $config{$func}->{'fetch'}($func, $ups); } exit 0;