[Nut-upsuser] A logging/graphing UPS client using rrdtool and UPS::Nut

Brother Railgun of Reason alaric at caerllewys.net
Sun Sep 19 19:21:02 UTC 2010


I finally got around to writing myself a NUT UPS monitoring tool.  NUT's 
UPS monitoring CGI tools provide a detailed instantaneous view of the 
UPS's state at any moment, but provide no history.  The attached tool, 
upswatch, uses Kiss Gabor's UPS::Nut module and Tobi Oetiker's rrdtool 
to provide a graphical history of the major UPS operating parameters via 
a web browser.  Currently two views and three datasets are provided: UPS 
battery pack charge (percentage) and voltage, input and output current 
and voltage, and UPS percentage load, with a 24-hour view and a 
lower-resolution 7-day view.  Sufficient data points are stored to 
support view up to 180 days, but views longer than 7 days are not 
currently implemented.  This version monitors only a single UPS at a 
time.  An example HTML page to access the output graphs is included (you 
will at the very least need to update the upsstata.cgi link at the 
bottom to match your site configuration).





-- 
  Phil Stracchino, CDK#2     DoD#299792458     ICBM: 43.5607, -71.355
  alaric at caerllewys.net   alaric at metrocast.net   phil at co.ordinate.org
         Renaissance Man, Unix ronin, Perl hacker, Free Stater
                 It's not the years, it's the mileage.
-------------- next part --------------
#!/usr/bin/perl

# UPSwatch v0.99 (c) Babylon Communications 2010
# Phil Stracchino <phil at co.ordinate.org>
# This code is freely usable and redistributable, and should be
# considered to be multiply licensed under any applicable
# open-source license, specifically to include the GNU Lesser
# General Public License v2.1 or, at your option, any later version.
#
# This is development code, and does not include any particular
# warranty of fitness or suitability.  You may need to modify it
# for your specific application, and may freely do so.


use lib qw(/opt/rrdtool/lib/perl/5.8.9/i86pc-solaris-thread-multi/ /opt/rrdtool/lib/perl/5.8.9/);
use Getopt::Long;
use POSIX;
use RRDs;
use UPS::Nut;
use Pod::Usage;
use strict;

my $version	= '0.99';
my $desc	= "\nUPSwatch version $version : A NUT UPS monitoring tool using RRDtool\n";


my (%opts, $runuser, $pid,
    $username, $password, $upshost,
    $basedir, $htmldir, $rrd_file, $logfile,
    $daychargegraph, $weekchargegraph,
    $daypowergraph, $weekpowergraph,
    $dayloadgraph, $weekloadgraph);


if (GetOptions(\%opts,
               'foreground',
               'username=s',
               'password=s',
               'runas=s',
               'upshost=s',
               'upsname=s',
               'basedir=s',
               'htmldir=s',
               'logfile=s',
               'usage|?',
               'help',
               'man|manual'))
{

    if ($opts{usage})
    {
        pod2usage(-message    => $desc,
                  -exitstatus => 0,
                  -verbose    => 0);
    }
    elsif ($opts{help})
    {
        pod2usage(-message    => $desc,
                  -exitstatus => 0,
                  -verbose    => 1);
    }
    elsif ($opts{man})
    {
        pod2usage(-exitstatus => 0,
                  -verbose    => 2);
    }
    else
    {
        unless ($opts{foreground})
        {
      FORK: {
                if ($pid = fork)
                {
                    exit 0;
                }
                elsif (defined $pid)
                {
                    print "UPSwatch daemonizing...\n";
                    close (STDIN);
                    close (STDOUT);
                    close (STDERR);
                    upswatch();
                    exit 0;
                }
                elsif ($! =~ /No more process/)
                {
                    sleep 5;
                    redo FORK;
                }
                else
                {
                    die "Unable to fork: $!\n";
                }
            }
        }
        else
        {
            upswatch();
        }
    }
}
else
{
    pod2usage(-message    => $desc,
              -exitstatus => -1,
              -verbose    => 0);
}

exit (0);


sub upswatch
{
    my ($ups, $date, $vin, $vout, $iin, $iout,
        $bcharge, $bvolts, $load, $graphctr,
        $uid, $gid);

    set_defaults();

    open (LOG, ">>$logfile") unless ($opts{foreground});
    select((select(LOG), $| = 1)[0]);

    (undef,undef,$uid,$gid) = getpwnam($runuser);

    create_files($uid,$gid);

    POSIX::setgid($gid) || upslog("Could not set GID!\n");
    POSIX::setuid($uid) || upslog("Could not set UID!\n");

    create_rrds($rrd_file) unless (-e $rrd_file);
    $graphctr = create_graphs();

    while (1)
    {
        $ups = UPS::Nut->new(NAME     => $opts{upsname},
                             HOST     => $upshost);

        $ups->Authenticate($username, $password) || upslog("Could not authenticate to UPSD!\n");

        $date    = time();
        $bcharge = $ups->GetVar('battery.charge')  || upslog($ups->Error());
        $bvolts  = $ups->GetVar('battery.voltage') || upslog($ups->Error());
        $vin     = $ups->GetVar('input.voltage')   || upslog($ups->Error());
        $vout    = $ups->GetVar('output.voltage')  || upslog($ups->Error());
        $iin     = $ups->GetVar('input.current')   || upslog($ups->Error());
        $iout    = $ups->GetVar('output.current')  || upslog($ups->Error());
        $load    = $ups->GetVar('ups.load')        || upslog($ups->Error());

        $bcharge =~ s/\s+//g;
        $bvolts  =~ s/\s+//g;
        $vin     =~ s/\s+//g;
        $vout    =~ s/\s+//g;
        $iin     =~ s/\s+//g;
        $iout    =~ s/\s+//g;
        $load    =~ s/\s+//g;

        RRDs::update($rrd_file, "$date:$bcharge:$bvolts:$vin:$vout:$iin:$iout:$load");

        upslog(sprintf("Data point at %d (%s): charge %2.1f% (%2.1f)\n",
                       $date,
                       strftime("%a %R", localtime($date)),
                       $bcharge,
                       $bvolts));

        $graphctr = create_graphs() unless (--$graphctr);

        # it would be good to close the connection here, but UPS::Nut
        # doesn't provide a method to do so.  It would arguably be
        # BETTER to use a single upsd connection and hold it open, but
        # experience shows that the connection times out after 90 to
        # 120 minutes.

        select(undef,undef,undef,60);
    }

    # we should terminate only if killed, in which case Perl should clean
    # up after us, but let's be well-behaved and close the log anyway

    close (LOG);
    return;
}


sub set_defaults
{
    $runuser    = $opts{runas}    || 'upswatch';
    $username   = $opts{username} || 'upswatch';
    $password   = $opts{password} || 'upswatch';
    $upshost    = $opts{upshost}  || 'localhost';
    $basedir    = $opts{basedir}  || '/usr/local/var/upswatch';
    $htmldir    = $opts{htmldir}  || '/var/httpd/local/upswatch';
    $logfile    = $opts{logfile}  || '/var/log/upswatch.log';

    $rrd_file        = $basedir.'/upscharge.rrd';
    $daychargegraph  = $htmldir.'/upscharge-day.png';
    $weekchargegraph = $htmldir.'/upscharge-week.png';
    $daypowergraph   = $htmldir.'/upspower-day.png';
    $weekpowergraph  = $htmldir.'/upspower-week.png';
    $dayloadgraph    = $htmldir.'/upsload-day.png';
    $weekloadgraph   = $htmldir.'/upsload-week.png';

    return;
}


sub create_files
{
    my ($uid, $gid) = @_;

    foreach my $dir ($basedir, $htmldir)
    {
        unless (-d $dir)
        {
            system(split(/\s+/, "mkdir -p $dir"));
            chown($uid, $gid, $dir);
        }
    }

    foreach my $file ($daychargegraph, $weekchargegraph,
                      $daypowergraph, $weekpowergraph,
                      $dayloadgraph, $weekloadgraph)
    {
        unless (-f $file)
        {
            system(split(/\s+/, "touch $file"));
            chown($uid, $gid, $file);
        }
    }
}


sub create_rrds
{
    my ($rrd_file) = $_[0];

    
    RRDs::create($rrd_file, "--step", 60,
                 "DS:bcharge:GAUGE:120:0:100",
                 "DS:bvolts:GAUGE:120:0:150",
                 "DS:vinput:GAUGE:120:0:150",
                 "DS:voutput:GAUGE:120:0:150",
                 "DS:iinput:GAUGE:120:0:30",
                 "DS:ioutput:GAUGE:120:0:30",
                 "DS:load:GAUGE:120:0:125",
                 "RRA:AVERAGE:0.5:1:2880",
                 "RRA:AVERAGE:0.8:5:2880",
                 "RRA:AVERAGE:0.9:60:4320") || upslog("RRDs not created properly!\n");

    return (-e $rrd_file);
}


sub create_graphs
{
    RRDs::graph($daychargegraph,
                "--title",		"Battery Pack Charge (last 24 hours)",
                "--upper-limit",	150,
                "--lower-limit",	0,
                "--height",		200,
                "--x-grid",		"HOUR:1:HOUR:3:HOUR:3:0:%R",
                "--right-axis",		"1:0",
                "--rigid",
                "DEF:charge=$rrd_file:bcharge:AVERAGE",
                "DEF:voltage=$rrd_file:bvolts:AVERAGE",
                "LINE2:charge#0000FF:Battery Charge (percent)",
                "LINE2:voltage#00FF00:Battery Charge (volts)") || upslog("$daychargegraph update failed!\n");

    RRDs::graph($weekchargegraph,
                "--title",		"Battery Pack Charge (last 7 days)",
                "--start",		time()-604800,
                "--upper-limit",	150,
                "--lower-limit",	0,
                "--height",		200,
                "--x-grid",		"HOUR:3:DAY:1:DAY:1:86400:%a",
                "--right-axis",		"1:0",
                "--rigid",
                "DEF:charge=$rrd_file:bcharge:AVERAGE",
                "DEF:voltage=$rrd_file:bvolts:AVERAGE",
                "LINE2:charge#0000FF:Battery Charge (percent)",
                "LINE2:voltage#FF0000:Battery Charge (volts)") || upslog("$weekchargegraph update failed!\n");

    RRDs::graph($daypowergraph,
                "--title",		"Input/Output Voltage/Current (last 24 hours)",
                "--lower-limit",	0,
                "--height",		200,
                "--x-grid",		"HOUR:1:HOUR:3:HOUR:3:0:%R",
                "--right-axis",		"1:0",
                "DEF:vinput=$rrd_file:vinput:AVERAGE",
                "DEF:voutput=$rrd_file:voutput:AVERAGE",
                "DEF:iinput=$rrd_file:iinput:AVERAGE",
                "DEF:ioutput=$rrd_file:ioutput:AVERAGE",
                "LINE2:vinput#FF0000:Volts In",
                "LINE2:voutput#000000:Volts Out",
                "LINE2:iinput#0000FF:Amps In",
                "LINE2:ioutput#00FF00:Amps Out") || upslog("$daypowergraph update failed!\n");

    RRDs::graph($weekpowergraph,
                "--title",		"Input/Output Voltage/Current (last 7 days)",
                "--start",		time()-604800,
                "--lower-limit",	0,
                "--height",		200,
                "--x-grid",		"HOUR:3:DAY:1:DAY:1:86400:%a",
                "--right-axis",		"1:0",
                "DEF:vinput=$rrd_file:vinput:AVERAGE",
                "DEF:voutput=$rrd_file:voutput:AVERAGE",
                "DEF:iinput=$rrd_file:iinput:AVERAGE",
                "DEF:ioutput=$rrd_file:ioutput:AVERAGE",
                "LINE2:vinput#FF0000:Volts In",
                "LINE2:voutput#000000:Volts Out",
                "LINE2:iinput#0000FF:Amps In",
                "LINE2:ioutput#00FF00:Amps Out") || upslog("$weekpowergraph update failed!\n");

    RRDs::graph($dayloadgraph,
                "--title",		"UPS Load (last 24 hours)",
                "--lower-limit",	0,
                "--height",		200,
                "--x-grid",		"HOUR:1:HOUR:3:HOUR:3:0:%R",
                "--right-axis",		"1:0",
                "DEF:load=$rrd_file:load:AVERAGE",
                "CDEF:low=load,50,LT,load,0,IF",
                "CDEF:med=load,50,GE,load,0,IF",
                "CDEF:high=load,80,GE,load,0,IF",
                "CDEF:crit=load,100,GE,load,0,IF",
                "AREA:low#00FF00:Low (<50)",
                "AREA:med#0000FF:Medium (<80)",
                "AREA:high#FFFF00:High (>80)",
                "AREA:crit#FF0000:Critical (>100)",
                "LINE1:load#000000") || upslog("$dayloadgraph update failed!\n");

    RRDs::graph($weekloadgraph,
                "--title",		"UPS Load (last 7 days)",
                "--start",		time()-604800,
                "--lower-limit",	0,
                "--height",		200,
                "--x-grid",		"HOUR:3:DAY:1:DAY:1:86400:%a",
                "--right-axis",		"1:0",
                "DEF:load=$rrd_file:load:AVERAGE",
                "CDEF:low=load,50,LT,load,0,IF",
                "CDEF:med=load,50,GE,load,0,IF",
                "CDEF:high=load,80,GE,load,0,IF",
                "CDEF:crit=load,100,GE,load,0,IF",
                "AREA:low#00FF00:Low (<50)",
                "AREA:med#0000FF:Medium (<80)",
                "AREA:high#FFFF00:High (>80)",
                "AREA:crit#FF0000:Critical (>100)",
                "LINE1:load#000000") || upslog("$weekloadgraph update failed!\n");

    upslog(sprintf("Graphs updated at %d (%s)\n",
                   time(),
                   strftime("%a %R", localtime(time()))));

    return (5);
}


sub upslog
{
    $opts{foreground} ? print $_[0] : print LOG $_[0];

    return;
}

__END__

=head1 NAME

B<upswatch> - A NUT UPS monitoring tool using rrdtool

=head1 VERSION

Version 0.99

=head1 SYNOPSIS

upswatch [options]

  Options:
    -foreground
    -runas <system username>
    -username <NUT user>
    -password <NUT password>
    -upshost <hostname>
    -upsname <UPS name>
    -basedir <dir>
    -htmldir <dir>
    -logfile <file>
    -usage, -?
    -help
    -manual

=head1 OPTIONS

=over 4

=item B<-foreground>

Run in the foreground instead of daemonizing, directing all output
to STDOUT instead of writing to a log file.

=item B<-runas user>

Drop privileges and run as the specified system user after creating
required files and directories.  Defaults to 'upswatch'.

=item B<-username user>

=item B<-password pass>

Username and password to use when connecting to upsd.  This should
be an unprivileged slave user.  Defaults to upswatch/upswatch.

=item B<-upshost host>

=item B<-upsname name>

Name and host of the UPS to monitor.  Host defaults to localhost;
name has no default.

=item B<-basedir dir>

The 'home directory' in which upswatch will create the RRD data file.
It will be created on first run if it does not already exist.  Defaults
to /usr/local/var/upswatch.

=item B<-htmldir dir>

The directory in which to create graphs.  It will be created on first
run if it does not already exist.  Defaults to /var/httpd/local/upswatch.

=item B<-logfile file>

Location of the upswatch system log file.  The log is opened before
dropping privileges.  It is assumed the log directory already exists.

=item B<-usage, -?>

Display a brief usage summary.

=item B<-help>

Display more verbose information about usage and options.

=item B<-manual>

Display complete information about B<UPSwatch>. 

=back

=head1 DESCRIPTION

B<UPSwatch> is a tool for logging UPS operating parameters over time using
a round-robin database.  It uses B<NUT>'s B<upsd>, Tobi Oetiker's B<rrdtool>, and
Kiss Gabor's B<UPS::Nut> module.  It collects data on a single UPS from B<upsd>
once per minute, storing 72 hours of high-resolution samples, ten days of
five-minute averages, and 180 days of one-hour averages.  The data collected
consists of UPS battery pack voltage, UPS charge percentage, UPS load
percentage, supply voltage and current, and output voltage and current.  It
uses rrdtool to generate three sets of graphs charting these parameters in
a 24-hour high-resolution view and a 7-day medium-resolution view.

This is a proof-of-concept tool, and is written specifically for the operating
parameters of a HP R3000XR UPS, which is a transformerless DSP design using a
line-voltage battery pack.  You may find you need to modify some graph parameters
if you have a transformer-based UPS using a low-voltage battery pack.

You B<MUST> must specify the name of the UPS to monitor, and should set up
an unprivileged user, both on your system and for upsd, to be used by B<UPSwatch>.
If your UPS is not connected directly to localhost, you will need to specify
the host for your UPS, just as you would with any other B<NUT> client.  All other
parameters can probably be left at their defaults, or can be set as you desire.

Upon startup, B<UPSwatch> will daemonize unless told to run in the foreground,
create all required files and directories if they do not already exist, and
set their uid and gid to the uid and gid of the 'runas' user.  If not told to
run in the foreground, it will then open its logfile in append mode.  Finally,
it will drop privileges, create an initial set of graphs (which will be blank
to start with on the first run), and begin monitoring the UPS.  Graphs will be
updated every five minutes, and can ve viewed directly or via a separate Web
page.  Setting up the Web interface is at this time left to the end user.

=head1 REPORTING BUGS

Please send all bug reports to the author.

=head1 LICENSE

B<UPSwatch> is free software.  You may redistribute and/or modify it under the
terms of the GNU Lesser General Public License as published by the Free Software
Foundation; either version 2.1 of the License, or (at your option) any later
version; or under any other compatible open software license.


=head1 AUTHOR

B<UPSwatch> is written and maintained by Phil Stracchino (phil at co.ordinate.org).

=cut
 
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.alioth.debian.org/pipermail/nut-upsuser/attachments/20100919/6a4605bc/attachment-0001.html>


More information about the Nut-upsuser mailing list