#!/usr/bin/perl
#
# Copyright (c) 2011 Jakub Jirutka (jakub@jirutka.cz)
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or (at your
# option) any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for
# more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see .
#
###############################################################################
#
# Net-SNMP module for apcupsd
#
#
# Net-SNMP module for monitoring APC UPSes without SNMP support. It reads output
# from apcupsd (/sbin/apcaccess) and writes it into appropriate OIDs like UPSes
# with built-in SNMP support.
#
#
# To load this into a running agent with embedded perl support turned on, simply
# put the following line to your snmpd.conf file:
#
# perl do "/path/to/mod_apcupsd.pl";
#
# Net-snmp must be compiled with Perl support and apcupsd properly configured
# and running!
#
#
# You can download MIB file of PowerNet (APC) from
# http://www.michaelfmcnamara.com/files/mibs/powernet401.mib
#
# OID numbers for PowerNet-MIB: http://www.oidview.com/mibs/318/PowerNet-MIB.html
#
# If you want to edit this, set tab size in your editor to 4!
#
#
# @author: Jakub Jirutka
# @version: 1.0
# @date: 2011-07-31
#
BEGIN {
print STDERR "Starting mod_apcupsd.pl\n";
$agent || die "No \$agent defined\n";
}
use feature ('switch');
use NetSNMP::OID (':all');
use NetSNMP::agent (':all');
use NetSNMP::ASN (':all');
#################### SETTINGS ###################
# Set to 1 to get extra debugging information.
$debugging = 0;
# How often fetch data from /sbin/apcaccess (in seconds)?
my $fetch_interval = 20;
# Base OID of APC UPS tree to hook onto.
my $base_oid = '.1.3.6.1.4.1.318.1.1.1';
# OIDs mapping
my $mapping = [
# Apcupsd name OID suffix Data type OID name
['MODEL', '1.1.1.0', ASN_OCTET_STR], # upsBasicIdentModel
['UPSNAME', '1.1.2.0', ASN_OCTET_STR], # upsBasicIdentName
['FIRMWARE', '1.2.1.0', ASN_OCTET_STR], # upsAdvIdentFirmwareRevision
['SERIALNO', '1.2.3.0', ASN_OCTET_STR], # upsAdvIdentSerialNumber
['TONBATT', '2.1.2.0', ASN_TIMETICKS], # upsBasicBatteryTimeOnBattery
['BATTDATE', '2.1.3.0', ASN_OCTET_STR], # upsBasicBatteryLastReplaceDate
['BCHARGE', '2.2.1.0', ASN_GAUGE], # upsAdvBatteryCapacity
['ITEMP', '2.2.2.0', ASN_GAUGE], # upsAdvBatteryTemperature
['TIMELEFT', '2.2.3.0', ASN_TIMETICKS], # upsAdvBatteryRunTimeRemaining
['NOMBATTV', '2.2.7.0', ASN_INTEGER], # upsAdvBatteryNominalVoltage
['BATTV', '2.2.8.0', ASN_INTEGER], # upsAdvBatteryActualVoltage //should be ASN_INTEGER according to new ver. of MIB
['LINEV', '3.2.1.0', ASN_GAUGE], # upsAdvInputLineVoltage
['LINEFREQ', '3.2.4.0', ASN_GAUGE], # upsAdvInputFrequency
['LASTXFER', '3.2.5.0', ASN_INTEGER], # upsAdvInputLineFailCause
['OUTPUTV', '4.2.1.0', ASN_GAUGE], # upsAdvOutputVoltage
['LOADPCT', '4.2.3.0', ASN_GAUGE], # upsAdvOutputLoad
['NOMOUTV', '5.2.1.0', ASN_INTEGER], # upsAdvConfigRatedOutputVoltage
['HITRANS', '5.2.2.0', ASN_INTEGER], # upsAdvConfigHighTransferVolt
['LOTRANS', '5.2.3.0', ASN_INTEGER], # upsAdvConfigLowTransferVolt
['ALARMDEL', '5.2.4.0', ASN_INTEGER], # upsAdvConfigAlarm
['RETPCT', '5.2.6.0', ASN_INTEGER], # upsAdvConfigMinReturnCapacity
['SENSE', '5.2.7.0', ASN_INTEGER], # upsAdvConfigSensitivity
['MINTIMEL', '5.2.8.0', ASN_TIMETICKS], #? upsAdvConfigLowBatteryRunTime
['DWAKE', '5.2.9.0', ASN_TIMETICKS], # upsAdvConfigReturnDelay
['DSHUTD', '5.2.10.0', ASN_TIMETICKS], # upsAdvConfigShutoffDelay
['STESTI', '7.2.1.0', ASN_INTEGER], # upsAdvTestDiagnosticSchedule
['SELFTEST', '7.2.3.0', ASN_INTEGER], # upsAdvTestDiagnosticsResults //according to apcstatus.c, or date and time of last self test according to manual?!
['STATUS', '4.1.1.0', ASN_INTEGER] # upsBasicOutputStatus
];
# Maps apcupsd values to enum types according to MIB.
# Mainly based on apcupsd sources (apcstatus.c, drv_powernet.c) and PowerNet MIB.
my %enums = (
# STATUS => upsBasicOutputStatus
"$base_oid.4.1.1.0" => {
'UNKNOWN' => 1, # unknown
'ONLINE' => 2, # onLine
'ONBATT' => 3, # onBattery
'BOOST' => 4, # onSmartBoost
'TRIM' => 12, # onSmartTrim
},
# SELFTEST => upsAdvTestDiagnosticsResults
"$base_oid.7.2.3.0" => {
'OK' => 1, # ok
'BT' => 2, # failed //it's NOT in drv_powernet.c
'NG' => 3, # invalidTest
'IP' => 4, # testInProgress
'NO' => undef, # ! NONE
'WN' => undef, # ! WARNING
'??' => undef # ! UNKNOWN
},
# STESTI => upsAdvTestDiagnosticSchedule
"$base_oid.7.2.1.0" => {
'None' => 1, # unknown
'336' => 2, # biweekly
'168' => 3, # weekly
'ON' => 4, # atTurnOn
'OFF' => 5 # never
},
# SENSE => upsAdvConfigSensitivity
"$base_oid.5.2.7.0" => {
'Auto Adjust' => 1, # auto
'Low' => 2, # low
'Medium' => 3, # medium
'High' => 4, # high
'Unknown' => undef
},
# ALARMDEL -> upsAdvConfigAlarm
"$base_oid.5.2.4.0" => {
'30 seconds' => 1, # timed
'5 seconds' => 1, # timed
'Always' => 1, # timed
'Low Battery' => 2, # atLowBattery
'No alarm' => 3 # never
},
# LASTXFER => upsAdvInputLineFailCause
"$base_oid.3.2.5.0" => {
'No transfers since turnon' => 1, # noTransfer
'High line voltage' => 2, # highLineVoltage
'Low line voltage' => 4, # blackout
'Line voltage notch or spike' => 8, # largeMomentarySpike
'Automatic or explicit self test' => 9, # selfTest
'Unacceptable line voltage changes' => 10, # rateOfVoltageChange
'Forced by software' => undef,
'Input frequency out of range' => undef,
'UNKNOWN EVENT' => undef
}
);
# TODO upsBasicBatteryStatus, NOMPOWER
#################### INITIALIZATION ###################
# Hashmap for apcupsd names => OIDs
my %name_oid;
# Hashmap for OID => types
my %oid_type;
# Build hashmaps
foreach my $row (@$mapping) {
my ($name, $oid, $type) = @$row;
$oid = "$base_oid.$oid";
$name_oid{$name} = $oid;
$oid_type{$oid} = $type;
}
# Delete mapping array (we have hashmaps now)
undef $mapping;
# Timestamp of last data fetch
my $last_fetch;
# Fetched values from /sbin/apcaccess
my %data;
# Fetch data for the first time so we can build
# OID chain for actually available values.
&fetch_data;
# Chain of our OIDs in lexical order for GETNEXT
my %oid_chain;
# First OID in chain
my $first_oid = 0;
# Build OID chain
my $prev_oid;
foreach my $oid (&oid_lex_sort(keys(%data))) {
if (!$first_oid) {
$first_oid = $oid;
} else {
$oid_chain{$prev_oid} = $oid;
}
$prev_oid = $oid;
}
# Base OID to register
$reg_oid = new NetSNMP::OID($base_oid);
# Register in the master agent we're embedded in.
$agent->register('mod_apcupsd', $reg_oid, \&snmp_handler);
print STDERR "Registering at $base_oid \n" if ($debugging);
#################### SUBROUTINES ###################
# Fetch data from /sbin/apcaccess and convert for SNMP.
# This routine stores values in variable %data and fetch it again
# only when it's called after more than $fetch_interval seconds
# since last fetch.
sub fetch_data {
my $elapsed = time() - $last_fetch;
if ($elapsed < $fetch_interval) {
print STDERR "It's $elapsed sec since last update, interval is "
. "$fetch_interval\n" if ($debugging);
return 0;
}
print STDERR "Fetching data from /sbin/apcaccess\n" if ($debugging);
open AC, '/sbin/apcaccess status localhost |'
|| die "FATAL: can't run \"/sbin/apcaccess\": $!\n";
my $line;
while (defined($line = )) {
chomp $line;
if ($line !~ /^(\w+)\s*:\s*(.*\w)/) { next; }
my $oid = $name_oid{$1};
my $value = &convert_value($oid, $2) if $oid;
$data{$oid} = $value if (defined $value);
}
close AC;
$last_fetch = time();
return 1;
}
# Convert given raw value from /sbin/apcaccess to proper SNMP value according
# to data type defined in MIB.
# Given value must be without beginning and end whitespaces and only *value*,
# not whole row!
sub convert_value {
my ($oid, $raw) = @_;
# Convert values representing enums
# If enum value is undef, returns 0.
if (exists $enums{$oid}) {
my $enum = $enums{$oid}{$raw};
return (defined $enum ? $enum : 0);
}
# Convert other values according to their data type in MIB
given ($oid_type{$oid}) {
when ([ASN_INTEGER]) {
return ($raw =~ /(\d+)/g)[0];
}
when ([ASN_GAUGE]) {
return ($raw =~ /(\d+(?:\.\d+)?)/g)[0];
}
when ([ASN_TIMETICKS]) {
my ($val, $unit) = ($raw =~ /(\d+(?:\.\d+)?)\s+(\w+)/g);
return &convert_time($val, $unit);
}
default {
return $raw;
}
}
}
# Convert time in given unit (seconds, minutes or hours) to miliseconds.
sub convert_time {
my ($val, $unit) = @_;
given ($unit) {
when (m/^seconds/i) {
return $val * 100;
}
when (m/^minutes/i) {
return $val * 6000;
}
when (m/^hours/i) {
return $val * 360000;
}
default {
return $val;
}
}
}
# Subroutine that handle the incoming requests to our part of the OID tree.
# This subroutine will get called for all requests within the OID space
# under the registration oid made above.
sub snmp_handler {
my ($handler, $registration_info, $request_info, $requests) = @_;
my $request;
print STDERR "refs: ", join(", ", ref($handler), ref($registration_info),
ref($request_info), ref($requests)), "\n" if ($debugging);
print STDERR "Processing a request of type "
. $request_info->getMode() . "\n" if ($debugging);
&fetch_data;
for($request = $requests; $request; $request = $request->next()) {
# This is way how to convert NetSNMP::OID to numeric OID
my $oid = '.' . join('.', $request->getOID()->to_array());
print STDERR "Processing request of $oid\n" if ($debugging);
# Mode GET (for single entry)
if ($request_info->getMode() == MODE_GET) {
if (exists($data{$oid})) {
my $value = $data{$oid};
print STDERR " Returning: $value\n" if ($debugging);
$request->setValue($oid_type{$oid}, $value);
# Workaround for requests without "index"
} elsif (exists($data{"$oid.0"})) {
my $new_oid = "$oid.0";
my $value = $data{$new_oid};
print STDERR " Returning for $new_oid: $value\n" if ($debugging);
$request->setOID($new_oid);
$request->setValue($oid_type{$new_oid}, $value);
}
# Mode GETNEXT (for walking)
} elsif ($request_info->getMode() == MODE_GETNEXT) {
if (exists($oid_chain{$oid})) {
my $next_oid = $oid_chain{$oid};
my $value = $data{$next_oid};
print STDERR " Returning next OID $next_oid: $value\n" if ($debugging);
$request->setOID($next_oid);
$request->setValue($oid_type{$next_oid}, $value);
} elsif ($request->getOID() <= $reg_oid) {
my $value = $data{$first_oid};
print STDERR " Returning first OID $first_oid: $value\n" if ($debugging);
$request->setOID($first_oid);
$request->setValue($oid_type{$first_oid}, $value);
} else {
print STDERR "Illegal request\n" if ($debugging);
}
}
}
print STDERR "Processing finished\n" if ($debugging);
}
# Sort OIDs lexicographically
# See http://www.perlmonks.org/?node_id=524035
sub oid_lex_sort(@) {
return @_ unless (@_ > 1);
map { $_->[0] }
sort { $a->[1] cmp $b->[1] }
map {
my $oid = $_;
$oid =~ s/^\.//o;
$oid =~ s/ /\.0/og;
[$_, pack('N*', split('\.', $oid))]
} @_;
}