power.in 11.5 KB
Newer Older
1
#!/usr/bin/perl -wT
Leigh Stoller's avatar
Leigh Stoller committed
2 3 4

#
# EMULAB-COPYRIGHT
5
# Copyright (c) 2000-2006 University of Utah and the Flux Group.
Leigh Stoller's avatar
Leigh Stoller committed
6 7 8
# All rights reserved.
#

9 10 11
#
# Testbed Power Control script
#
12
# power [on|off|cycle] <node> [<node>] ...
13 14 15
#
############################################################

16 17 18
#
# Configure variables
#
19 20 21 22 23 24
my $TB         = "@prefix@";
my $TBOPS      = "@TBOPSEMAIL@";
my $ELABINELAB = @ELABINELAB@;
my $RPCSERVER  = "@OUTERBOSS_NODENAME@";
my $RPCPORT    = "@OUTERBOSS_XMLRPCPORT@";
my $RPCCERT    = "@OUTERBOSS_SSLCERTNAME@";
25

26
use lib "@prefix@/lib";
27
use libdb;
28
use libxmlrpc;
29
use power_rpc27;
30
use power_sgmote;
31
use power_mail;
32
use power_whol;
33
use power_rmcp;
34
use snmpit_apc;
35
use libtestbed;
36
use NodeType;
37
use strict;
38
use English;
39 40 41 42 43 44 45 46 47 48
use Getopt::Std;

sub usage() {
    print << "END";
Usage: $0 [-v n] [-e] <on|off|cycle> <node ...>
-e     Surpress sending of event - for use by scripts that have already sent it
-v n   Run with verbosity level n
END
    1;
}
49

50 51 52
#
# Un-taint path since this gets called from setuid scripts.
#
53
$ENV{'PATH'} = '/bin:/usr/bin:/usr/local/bin:@prefix@/bin';
54 55
delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};

56 57 58 59 60 61
my $op = "";			#stores operation (on/off/cyc)
my @machines = ();		#stores machines to operate on
my $ip = "";			#stores IP of a power controller
my $outlet = 0;			#stores number of an outlet
my %IPList = ();		#holds machine/ip pairs
my %OutletList = ();		#holds machine/outlet pairs
62
my $exitval = 0;
63

64 65 66
# Protos
sub dostatus(@);

67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88
#
# Process command-line arguments
#
my %opt = ();
getopts("v:he",\%opt);

if ($opt{h}) {
    exit &usage;
}

# useful values are 0, 1, 2 and 3
my $verbose = 0;
if ($opt{v}) {
    $verbose = $opt{v};
}
print "VERBOSE ON: Set to level $verbose\n" if $verbose;

my $sendevent = 1;
if ($opt{e}) {
    $sendevent = 0;
}

89 90 91
#
# Must have at least an op and a machine, so at least 2 ARGV
#
92 93 94 95
if (@ARGV < 2) {
    exit &usage;
}

96

97 98 99
#
# Read in ARGV
#
100
$op = shift (@ARGV);
101
if ($op =~ /^(on|off|cycle|status)$/) {
102
    $op = $1;
103
} else {
104
    exit &usage;
105
}
106 107 108 109

#
# Untaint the arguments.
#
110
@machines = @ARGV;
111
foreach my $n (0..$#ARGV) {
112
    $machines[$n] =~ s/^([-\@\w.]+)$/$1/;
113
}
114 115 116 117

#
# Lowercase nodenames and remove duplicates
#
118 119
my %all_nodes = ();
foreach my $n (0..$#machines) {
120
    $all_nodes{"\L$machines[$n]"} = 1; # Lowercase it and use as hash key
121
}
122
@machines= sort keys %all_nodes;
123

124 125 126 127
#
# Dump the args
#
print "do \"$op\" to @machines\n" if $verbose > 1;
128

129 130 131 132 133 134 135 136 137 138
#
# Handle the status command which is not a per-node operation and not
# allowed by anyone except admins.
#
if ($op eq "status") {
    die("Only admins are allowed to query status\n")
	if ($UID != 0 && !TBAdmin($UID));
    exit(dostatus(@machines));
}

139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157
#
# ElabinElab is special; Do local permission checks, build up a node list
# and then contact the proxy to do the actual work. No perl bindings yet,
# so have to use the python client program. 
#
if ($ELABINELAB) {
    my @nodelist = ();
    
    foreach my $node (@machines) {
	if (!(($UID == 0) ||
	      TBNodeAccessCheck($UID, TB_NODEACCESS_POWERCYCLE, $node))) {
	    warn "You are not authorized to control $node. Skipping...\n";
	    next;
	}
	push(@nodelist, $node);
    }

    exit(0)
	if (! @nodelist);
158

159 160 161 162 163 164 165 166 167 168 169 170 171
    libxmlrpc::Config({"server"  => $RPCSERVER,
		       "verbose" => 1,
		       "cert"    => $RPCCERT,
		       "portnum" => $RPCPORT});

    my $rval = libxmlrpc::CallMethod("elabinelab", "power",
				     {"op"    => "$op",
				      "nodes" => join(",", @nodelist)});
    if (!defined($rval)) {
	exit(-1);
    }
    exit($rval);
}
172

Mac Newbold's avatar
Mac Newbold committed
173 174 175 176 177
#
# Get table of users <--> machines for those nodes, to make sure
# user is authorized to control the nodes
#

178 179
my %timelimited = ();

180 181 182 183 184 185 186
#
# Though TBNodeAccessCheck can check all nodes at once, we do it one at
# a time, so that we can get a list of all nodes we have access to. This
# is primarily to preserve the pre-libification behavior of power
#
my %outlets = ();
foreach my $node (@machines) {
187
    if (!(($UID == 0) || TBNodeAccessCheck($UID,TB_NODEACCESS_POWERCYCLE,$node))) {
188
	warn "You are not authorized to control $node. Skipping...\n";
189
	next;
190
    }
191

192
    my $result = DBQueryFatal("select o.power_id, o.outlet, " .
193
	"UNIX_TIMESTAMP(last_power), n.type " .
194 195 196 197 198 199 200 201 202
	"from outlets as o left join nodes as n on " .
	"(o.node_id = n.node_id) ".
	# Shark hack
	"or (n.node_id = concat(o.node_id,'-1')) " .
	# End shark hack
	"left join node_types as t on n.type=t.type ".
	"where o.node_id='$node'");
    if ($result->num_rows() == 0) {
	warn "No outlets table entry found for $node. Skipping...\n";
203 204 205 206
	SENDMAIL($TBOPS,
		 "No power outlet for $node",
		 "Unable to power '$op' $node; no outlets table entry!",
		 $TBOPS);
207
	next;
208
    }
Mac Newbold's avatar
Mac Newbold committed
209

210 211 212 213
    my ($power_id, $outlet, $last_power, $nodetype) = $result->fetchrow();
    my $typeinfo    = NodeType->Lookup($nodetype);
    my $power_delay = $typeinfo->power_delay();
    my $time_ok     = (time() - $power_delay > $last_power ? 1 : 0);
214 215 216

    #
    # Check for rate-limiting, and update the last power cycle time
217 218 219 220
    # if it's been long enough. Root gets to bypass the checks, and
    # we only update the timestamp if it is being turned on or cycled,
    # to allow off then on without waiting (unless the on is too close 
    # to a previos on/cycle command)
221
    #
222 223 224 225
    if ( $op ne "off" ) {
	if (! ($time_ok || ($UID == 0)) ) {
	    warn "$node was power cycled recently. Skipping...\n";
	    next;
226
	} elsif ( $power_id ne "mail" ) {
227 228 229
	    DBQueryFatal("update outlets set last_power=CURRENT_TIMESTAMP " .
			 "where node_id = '$node'");
	}
230
    }
231

232 233 234 235
    #
    # Associate this node with the power controller it is attached to
    #
    push @{$outlets{$power_id}}, [$node, $outlet];
236 237
}

238
print "machines= ",join(" ",@machines),"\n" if $verbose;
239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255
print "devices= ", join(" ",keys %outlets),"\n" if $verbose;

foreach my $power_id (keys %outlets) {

    #
    # Get the list of outlet numbers used on this power controller
    #
    my @outlets = ();
    my @nodes = ();
    foreach my $node (@{$outlets{$power_id}}) {
	my ($node_id, $outlet) = @$node;
	push @outlets, $outlet;
	push @nodes, $node_id;

    }
    my $nodestr = join(",",@nodes);

256 257 258 259
    my $type;
    my $IP;
    my $class;

260 261
    if ($power_id eq "mail" || $power_id =~ /^whol-/ 
	|| $power_id=~ /^rmcp-/) {
262
	$type = $power_id;
263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281
	$IP = "";
	$class = "";
    }
    else {
	#
	# Find out some information about this power controller
	#
	my $result = DBQueryFatal("select n.type, i.IP, t.class ". 
		"from nodes as n " .
		"left join interfaces as i on n.node_id=i.node_id " .
		"left join node_types as t on n.type=t.type " .
		"where n.node_id='$power_id'");
	if ($result->num_rows() == 0) {
	    warn "No entry found for power controller $power_id. Skipping " .
		"$nodestr\n";
	    $exitval++;
	    next;
	}
	($type, $IP, $class) = $result->fetchrow();
282
    }
283 284 285 286 287 288 289 290 291

    #
    # Finally, we look at the controller type and construct the proper type
    # of object
    #
    my $errors = 0;
    if ($type eq "APC") {
	my $device = new snmpit_apc($IP,$verbose);
	if (!defined $device) {
292
	    warn "Unable to contact controller for $nodestr. Skipping...\n";
293
	    next;
294
	} else {
295 296 297 298 299 300 301
	    print "Calling device->power($op,@outlets)\n"
		if $verbose > 1;
	    if ($device->power($op,@outlets)) {
		print "Control of $nodestr failed.\n";
		$errors++;
	    }
	}
302
    } elsif ($type =~ "RPC") {
303 304
	if (rpc27ctrl($op,$power_id,@outlets)) {
	    print "Control of $nodestr failed.\n"; $exitval++;
305
	}
306 307 308 309 310 311 312
    } elsif (($class eq "sg") || ($type eq "garcia")) {
	# XXX: 'garcia' is temporary until stargates are subnodes of
	# garcias
	if (sgmotectrl($op,@nodes)) {
	    print "Control of $nodestr failed.\n"; $exitval++;
	    $errors++;
	}
313 314 315 316 317 318
    } elsif ($type =~ /whol-(\w+)/) {
	my $iface = $1;
	if (wholctrl($op,$iface,@nodes)) {
	    print "Control of $nodestr failed.\n"; $exitval++;
	    $errors++;
	}
319 320 321 322 323
    } elsif ($type =~ /rmcp-(\w+)/) {
	if (rmcpctrl($1,$op,@nodes)) {
	    print "Control of $nodestr failed.\n"; ++$exitval;
	    ++$errors;
	}
324 325 326 327 328
    } elsif ($type eq "mail") {
	if (mailctrl($op,@nodes)) {
	    print "Control of $nodestr failed.\n"; $exitval++;
	    $errors++;
	}
329
	$sendevent = 0; # power_mail sends this itself.
330
    } else {
331 332 333 334 335 336 337
	print "power: Unknown power type '$type'\n";
	$errors++;
    }

    if (!$errors) {
	foreach my $node (@nodes) {
	    print "$node now ",($op eq "cycle" ? "rebooting" : $op),"\n";
338
	    if ($sendevent) {
339 340
		my $state = TBDB_NODESTATE_SHUTDOWN;
		TBSetNodeEventState($node,$state);
341
	    }
342
	}
343 344
    } else {
	$exitval += $errors;
345
    }
346

347
}
348 349 350

# Return 0 on success. Return non-zero number of nodes that failed.
exit $exitval;
351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373

sub byname() {
    my ($as, $an, $bs, $bn);

    if ($a =~ /(.*[^\d])(\d+)$/) {
	$as = $1; $an = $2;
    } else {
	$as = $a;
    }
    if ($b =~ /(.*[^\d])(\d+)$/) {
	$bs = $1; $bn = $2;
    } else {
	$bs = $b;
    }
    $as cmp $bs || $an <=> $bn;
}

#
# Query the given controllers for their status
#
sub dostatus(@) {
    my @wanted = @_;
    my %ctrls = ();
374
    my %IPs = ();
375 376 377 378 379 380 381 382 383 384 385 386
    my $errors = 0;

    if ($ELABINELAB) {
	warn "Cannot get status from inner elab\n";
	return 1;
    }

    my $doall = (@wanted == 1 && $wanted[0] eq "all");

    #
    # Fetch all possible power controllers
    #
387 388 389 390 391 392
    my $result = DBQueryFatal("select n.node_id,t.type,i.IP ".
			"from nodes as n " .
			"left join node_types as t on n.type=t.type " .
			"left join interfaces as i on n.node_id=i.node_id " .
			"where n.role='powerctrl'");
    while (my ($ctrl, $type, $IP) = $result->fetchrow()) {
393
	$ctrls{$ctrl} = $type;
394
	$IPs{$ctrl} = $IP;
395 396 397 398 399
    }

    @wanted = sort byname keys(%ctrls)
	if ($doall);

Mike Hibler's avatar
Mike Hibler committed
400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425
    #
    # For anything that was specified that is not a power controller,
    # look it up as a node and discover its controller.
    # XXX this is not very efficient.
    #
    my @nwanted = ();
    for my $node (@wanted) {    
	my $ctrl;

	if (!defined($ctrls{$node})) {
	    $result = DBQueryFatal("select power_id,outlet from outlets ". 
				   "where node_id='$node'");
	    if (!$result || $result->numrows == 0) {
		warn "No such power controller '$node', ignored\n";
		$errors++;
		next;
	    } else {
		($ctrl, $outlet) = $result->fetchrow();
		print "$node is $ctrl outlet $outlet...\n";
	    }
	} else {
	    $ctrl = $node;
	}
	push(@nwanted, $ctrl);
    }

426 427 428
    #
    # Loop through desired controllers getting status
    #
Mike Hibler's avatar
Mike Hibler committed
429
    for my $ctrl (@nwanted) {
430 431
	my %status;

432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453
	if ($ctrls{$ctrl} eq "APC") {
	    my $device = new snmpit_apc($IPs{$ctrl}, $verbose);
	    if (!defined $device) {
		warn "Unable to contact controller $ctrl.\n";
		$errors++;
		next;
	    } else {
		print "Calling device->status()\n"
		    if $verbose > 1;
		if ($device->status(\%status)) {
		    print "Could not get status for $ctrl.\n";
		    $errors++;
		    next;
		}
	    }
	    for my $outlet (1..20) {
		my $ostr = "outlet$outlet";
		print "$ctrl Outlet $outlet: ", $status{$ostr}, "\n"
		    if (defined($status{$ostr}));
	    }
	    print "\n";
	} elsif ($ctrls{$ctrl} =~ /^RPC/) {
454 455 456 457 458
	    if (rpc27status($ctrl,\%status)) {
		print "Could not get status for $ctrl.\n";
		$errors++;
		next;
	    }
Mike Hibler's avatar
Mike Hibler committed
459
	    print "$ctrl Current: ", $status{current}, " Amps\n"
460
		if defined($status{current});
Mike Hibler's avatar
Mike Hibler committed
461
	    print "$ctrl Power: ", $status{power}, " Watts\n"
462
		if defined($status{power});
463 464 465 466 467
	    if (defined($status{tempF}) || defined($status{tempC})) {
		my $temp = $status{tempF};
		if (!defined($temp)) {
		    $temp = $status{tempC} * 9 / 5 + 32;
		}
Mike Hibler's avatar
Mike Hibler committed
468
		printf "$ctrl Temperature: %.1f F\n", $temp;
469 470 471
	    }
	    for my $outlet (1..20) {
		my $ostr = "outlet$outlet";
Mike Hibler's avatar
Mike Hibler committed
472
		print "$ctrl Outlet $outlet: ", $status{$ostr}, "\n"
473 474
		    if (defined($status{$ostr}));
	    }
Mike Hibler's avatar
Mike Hibler committed
475
	    print "\n";
476 477 478 479 480 481 482 483
	} elsif (!$doall) {
	    warn "Cannot get status for $ctrl (type " .
		$ctrls{$ctrl} . ") yet\n";
	    $errors++;
	}
    }
    return $errors;
}