power_apc.pm 8.3 KB
Newer Older
1
#!/usr/bin/perl -w
Leigh B. Stoller's avatar
Leigh B. Stoller committed
2 3

#
4
# Copyright (c) 2000-2018 University of Utah and the Flux Group.
5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
# 
# {{{EMULAB-LICENSE
# 
# This file is part of the Emulab network testbed software.
# 
# This file is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or (at
# your option) any later version.
# 
# This file 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 Affero General Public
# License for more details.
# 
# You should have received a copy of the GNU Affero General Public License
# along with this file.  If not, see <http://www.gnu.org/licenses/>.
# 
# }}}
Leigh B. Stoller's avatar
Leigh B. Stoller committed
24 25
#

26 27 28
#
# snmpit module for APC MasterSwitch power controllers
#
29
# supports new(ip), power(on|off|cyc[le],port), status
30 31 32 33 34 35 36
#

package snmpit_apc;

$| = 1; # Turn off line buffering on output

use SNMP;
37 38
use strict;

39
#
40 41
# XXX for configurations in which APC unit always returns error
# even when it works.
42 43 44 45 46 47 48
#
# NOTE: You can probably fix such units by instead making sure the
# controller uses the 'rPDUOutletControlOutletCommand' OID in power() below.
# The default 'sPDUOutletCtl' will work on these controllers but will return
# a '' status. I would guess that everything running "masterSwitch.6" and
# later should be using the newer OID.
#
49 50
my $ignore_errors = 0;

51 52 53 54 55 56 57 58 59 60 61 62 63 64
sub new($$;$) {

    # The next two lines are some voodoo taken from perltoot(1)
    my $proto = shift;
    my $class = ref($proto) || $proto;

    my $devicename = shift;
    my $debug = shift;

    if (!defined($debug)) {
	$debug = 0;
    }

    if ($debug) {
65
	print "snmpit_apc module initializing... debug level $debug\n";
66
    }
67

68
    $SNMP::debugging = ($debug - 5) if $debug > 5;
69 70
    my $mibpath = "/usr/local/share/snmp/mibs";
    &SNMP::addMibDirs($mibpath);
71 72 73 74
    &SNMP::addMibFiles("$mibpath/SNMPv2-SMI.txt",
		       "$mibpath/SNMPv2-MIB.txt",
		       "$mibpath/RFC1155-SMI.txt",
		       "$mibpath/PowerNet-MIB.txt");
75 76 77 78
    $SNMP::save_descriptions = 1; # must be set prior to mib initialization
    SNMP::initMib();              # parses default list of Mib modules
    $SNMP::use_enums = 1;         #use enum values instead of only ints
    print "Opening SNMP session to $devicename..." if $debug;
79
    my $sess =new SNMP::Session(DestHost => $devicename, Community => 'private', Version => '1');
80 81 82 83 84 85 86 87 88 89 90 91 92
    if (!defined($sess)) {
	warn("ERROR: Unable to connect to $devicename via SNMP\n");
	return undef;
    }

    my $self = {};

    $self->{SESS} = $sess;
    $self->{DEBUG} = $debug;
    $self->{DEVICENAME} = $devicename;

    bless($self,$class);
    return $self;
93 94
}

95
my %CtlOIDS = (
96 97 98 99
    default => ["sPDUOutletCtl",
		"outletOn", "outletOff", "outletReboot"],
    rPDU    => ["rPDUOutletControlOutletCommand",
		"immediateOn", "immediateOff", "immediateReboot"]
100 101
);

102
sub power {
103 104
    my $self = shift;
    my $op = shift;
105
    my @ports = @_;
106 107 108
    my $oids = $CtlOIDS{"default"};
    my $type = SNMP::translateObj($self->{SESS}->get("sysObjectID.0"));

109 110 111 112 113
    print "Got type '$type'\n" if $self->{DEBUG};
    if (defined($type)) {
	if ($type eq "masterSwitchrPDU") {
	    $oids = $CtlOIDS{"rPDU"};
	}
114 115 116 117 118
	# XXX wonky APC power controllers at the fort (AP7941) return
	# either masterSwitch.5 or masterSwitch.5.1.3.4.5
	elsif ($type =~ /^masterSwitch.5/) {
	    $oids = $CtlOIDS{"rPDU"};
	}
119 120 121 122
	# XXX newer APC power controllers we have (AP8941, AP7900B) need to
	# use this OID else they return an error on set operations (though
	# the operations do work!)
	elsif ($type eq "masterSwitch.6" || $type eq "masterSwitch.8") {
123 124 125
	    $oids = $CtlOIDS{"rPDU"};
	}
    }
126

127 128
#   "rPDUOutletControl" is ".1.3.6.1.4.1.318.1.1.12.3.3";
#   "sPDUOutletCtl"     is ".1.3.6.1.4.1.318.1.1.4.4.2.1.3";
129 130 131
    if    ($op eq "on")  { $op = @$oids[1]; }
    elsif ($op eq "off") { $op = @$oids[2]; }
    elsif ($op =~ /cyc/) { $op = @$oids[3]; }
132 133 134 135 136

    my $errors = 0;

    foreach my $port (@ports) {
	print STDERR "**** Controlling port $port\n" if ($self->{DEBUG} > 1);
137
	if ($self->UpdateField(@$oids[0],$port,$op)) {
138 139 140
	    print STDERR "Outlet #$port control failed.\n";
	    $errors++;
	}
141
    }
142 143

    return $errors;
144 145
}

146 147 148 149 150
sub status {
    my $self = shift;
    my $statusp = shift;
    my %status;

151
# status for AP7941: .1.3.6.1.4.1.318.1.1.12.3.5
152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171
    my $StatOID = ".1.3.6.1.4.1.318.1.1.4.2.2";
    my $Status = 0;

    $Status = $self->{SESS}->get([[$StatOID,0]]);
    if (!defined $Status) {
	print STDERR $self->{DEVICENAME}, ": no answer from device\n";
	return 1;
    }
    print("Status is '$Status'\n") if $self->{DEBUG};

    if ($statusp) {
	my @stats = split '\s+', $Status;
	my $o = 1;
	foreach my $ostat (@stats) {
	    my $outlet = "outlet$o";
	    $status{$outlet} = $ostat;
	    $o++;
	}
	%$statusp = %status;
    }
172 173 174 175 176 177 178 179 180 181 182

    #
    # We can retrieve the total amperage in use (in tenths of amps) 
    # on an APC by retrieving the rPDULoadStatusLoad.  There are 
    # entries for each of the phases of power that the device supports,
    # and for each of the banks of power it provides.
    #
    # We could add either the phases or the banks, but since the phases
    # come first, we use them.  We grab the number of phases supported,
    # then use that as a limit on how many status load values we retrieve.
    #
183 184 185
    # The OID to retrieve the phases is: ".1.3.6.1.4.1.318.1.1.12.1.9"
    # for more recent units, or:         ".1.3.6.1.4.1.318.1.1.12.2.1.2"
    # for older ones;
186
    # the load status table OID is:      ".1.3.6.1.4.1.318.1.1.12.2.3.1.1.2".
187
    #
188
    my ($phases,$banks);
189

190
    $phases = $self->{SESS}->get([["rPDUIdentDeviceNumPhases",0]]);
191
    if (!$phases) {
192 193 194 195 196 197 198 199
	# not all models support this MIB, try another
	$phases = $self->{SESS}->get([["rPDULoadDevNumPhases",0]]);
	if (!$phases) {
	    # some don't support either, bail.
	    print STDERR "Query phase: IdentDeviceNumPhases/LoadDevNumPhases failed\n"
		if $self->{DEBUG};
	    return 0;
	}
200 201
    }

202 203 204 205 206 207 208 209
    $banks = $self->{SESS}->get([["rPDULoadDevNumBanks",0]]);
    if (!$banks) {
	# not clear if we really need this, so just continue
	print STDERR "Query phase: LoadDevNumBanks failed\n"
	    if $self->{DEBUG};
	$banks = 0;
    }

210
    print "Okay.\nPhase report was '$phases'\n" if $self->{DEBUG};
211
    print "Bank report was '$banks'\n" if $self->{DEBUG};
212
    my ($varname, $index, $power, $val, $done);
213 214
    my %perphase = ();
    my %perbank = ();
215 216 217 218
    my $oid = ["rPDULoadStatusLoad",1];

    $self->{SESS}->get($oid);
    while ($$oid[0] =~ /rPDULoad/) {
219
	print "rPDULoadStatusLoad returns: ", join('/', @$oid), "\n" if $self->{DEBUG} > 1;
220 221 222 223 224
        ($varname, $index, $val) = @{$oid};
        if ($varname eq "rPDULoadStatusLoad") {
            if ($index <= $phases) {
    		print "Raw current value $val\n" if $self->{DEBUG};
                $status{current} += $val;
225 226 227 228
		$perphase{$index} = $val;
            } elsif ($phases == 1 && $banks > 0) {
		$perbank{$index-$phases} = $val;
	    }
229 230 231 232
        }
        $self->{SESS}->getnext($oid);
    }

233 234 235 236 237 238 239 240 241 242 243 244 245
    if ($self->{DEBUG}) {
	print "Total raw current is $status{current}\n";
	print "Phases:\n";
	foreach my $i (sort keys %perphase) {
	    print "$i: ", $perphase{$i} / 10, "\n";
	}
	if ($banks > 0) {
	    print "Banks:\n";
	    foreach my $i (sort keys %perbank) {
		print "$i: ", $perbank{$i} / 10, "\n";
	    }
	}
    }
246 247 248 249 250 251
    $status{current} /= 10;

    if ($statusp) {
       %$statusp = %status;
    }

252 253 254
    return 0;
}

255
sub UpdateField {
256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272
    my ($self,$OID,$port,$val) = @_;

    print "sess=$self->{SESS} $OID $port $val\n" if $self->{DEBUG} > 1;

    my $Status = 0;
    my $retval;

    print "Checking port $port of $self->{DEVICENAME} for $val..." if $self->{DEBUG};
    $Status = $self->{SESS}->get([[$OID,$port]]);
    if (!defined $Status) {
	print STDERR "Port $port, change to $val: No answer from device\n";
	return 1;
    } else {
	print "Okay.\nPort $port was $Status\n" if $self->{DEBUG};
	if ($Status ne $val) {
	    print "Setting $port to $val..." if $self->{DEBUG};
	    $retval = $self->{SESS}->set([[$OID,$port,$val,"INTEGER"]]);
273 274
	    $retval = "" if (!defined($retval));
	    print "Set returned '$retval'\n" if $self->{DEBUG};
275 276 277 278 279 280 281 282 283
	    if ($retval) {
		return 0;
	    }
	    # XXX warn, but otherwise ignore errors
	    if ($ignore_errors) {
		print STDERR "WARNING: $port '$val' failed, ignoring\n";
		return 0;
	    }
	    return 1;
284 285
	}
	return 0;
286 287 288 289 290
    }
}

# End with true
1;