#!/usr/bin/perl -wT # # EMULAB-COPYRIGHT # Copyright (c) 2000-2002, 2005 University of Utah and the Flux Group. # All rights reserved. # package power_rpc27; use Exporter; @ISA = ("Exporter"); @EXPORT = qw( rpc27status rpc27ctrl ); use Socket; use IO::Handle; use lib "@prefix@/lib"; use libdb; use POSIX qw(strftime); # A little perl module to power cycle something attached to an RPC27. # Thats a serially controlled, power controller. # # XXX The little secretkey handshake is coded in here. If this changes # in capture, you have to change it here too. Look for the "pack" # statement below. # # XXX Note that if someone has already tipped to the power controller # in question, this won't work at all. # Turn off line buffering on output $| = 1; # Set for more output. my $debug = 0; # RPC27 Prompt string my $RPC27_PROMPT = 'RPC-\d+>'; # RPC help message. Used as terminator for status fetch. my $RPC27_HELPMSG = 'Type "Help" for a list of commands'; my %RPC27_CMD = ("cycle" => "reboot", "on" => "on", "off" => "off"); sub rpc27status { my ($controller, $statusp) = @_; my($TIP, $i, $insync); # # Form the connection to the controller via a "tip" line to the # capture process. Once we have that, we can just talk to the # controller directly. # if (!($TIP = tipconnect($controller))) { print STDERR "*** Could not form TIP connection to $controller\n"; return 1; } if (syncandsend($controller, $TIP, "status", $statusp)) { return 1; } return 0; } # Main routine. # usage: rpc27ctrl(cmd, controller, outlet) # cmd = { "cycle" | "on" | "off" | "status" } # controller = # outlet = int, 1 <= outlet <= 20 # # Returns 0 on success. Non-zero on failure. # sub rpc27ctrl { my($cmd, $controller, @outlets) = @_; # # Check parameters # if (!defined($RPC27_CMD{$cmd})) { print STDERR "*** Undefined command: '$cmd'\n"; return 1; } if (grep {$_ < 1 || $_ > 20} @outlets) { print STDERR "*** Invalid outlet '$outlet': Must be 1-20\n"; return 1; } # # Make a comma-seperated strings of all the outlets to reboot. The RPCs # have a buffer limit of 31 characters, which limits us to 8 outlets # at a time (assuming the longest command and 2-digit outlet numbers) # my @outlet_strings = (); while (@outlets) { my @tmp_outlets = (); for (my $i = 0; ($i < 8) && (@outlets); $i++) { push @tmp_outlets,shift(@outlets); } push @outlet_strings, join(",",@tmp_outlets); } if ($debug) { print "outlet_strings: ", join(" ",map("($_)",@outlet_strings)), "\n"; } # # Form the connection to the controller via a "tip" line to the # capture process. Once we have that, we can just talk to the # controller directly. # if (! ($TIP = tipconnect($controller))) { print STDERR "*** Could not form TIP connection to $controller\n"; return 1; } foreach my $outlet (@outlet_strings) { my $command = "$RPC27_CMD{$cmd} $outlet"; if (syncandsend($controller,$TIP,$command,undef)) { # # On failure, syncandsend has already closed $TIP # return 1; } } close($TIP); return 0; } # # Sync up with the power controller, and set it a command. $controller is the # controller name, for error message purposes, $TIP is the connection to # the controller opened with tipconnect, and $command is the whole command # (ie. 'reboot 20,40') to send. # sub syncandsend($$$) { my ($controller,$TIP,$cmd,$statusp) = @_; # # Send a couple of newlines to get the command prompt, and then wait # for it to print out the command prompt. This loop is set for a small # number since if it cannot get the prompt quickly, then something has # gone wrong. # my $insync = 0; for (my $i = 0; $i < 20; $i++) { my $line; if (syswrite($TIP, "\r\n") == 0) { print STDERR "*** Power control sync write failed ($controller/$outlet)\n"; close($TIP); return 1; } $line = rpc_readline($TIP); if (!defined($line)) { print STDERR "*** Power control sync read failed ". "($controller/$outlet)\n"; close($TIP); return 1; } if ($debug) { print "Read: $line"; } if ($line =~ /$RPC27_PROMPT/) { if ($debug) { print "Matched prompt '$RPC27_PROMPT'!\n"; } $insync = 1; last; } } if (! $insync) { print STDERR "*** Could not sync with power controller! ". "($controller/$outlet)\n"; close($TIP); return 1; } # # These things have an annoying tendency to fail sometimes by losing # the command part of the string (e.g., "6" instead of "reboot 6"). # I think we are overrunning the UART with "\r\n" since we put out # a pair after every line we read that is not a prompt and when the # RPC puts out its banner, it is a good 10 lines before we see the # prompt. So allow some time for the RPC to digest everything before # we feed it the command. # sleep(1); if ($debug) { print "Sending '$cmd' to $controller\n"; } # Okay, got a prompt. Send it the string: if (syswrite($TIP, "$cmd\r\n") == 0) { print STDERR "*** Power control write failed ($controller/$outlet)\n"; close($TIP); return 1; } # If status is desired, slurp and parse everything up to the next prompt if ($statusp) { my %status = (); print "Getting status\n" if ($debug); while (my $line = rpc_readline($TIP)) { print "Read: $line" if ($debug); # skip echoed prompt+command if ($line =~ /status/) { next; } # XXX cannot look for final prompt because there is no newline if ($line =~ /$RPC27_HELPMSG/) { last; } if ($line =~ /Temperature:\s+(\d+\.\d+) C/) { $status{tempC} = $1; } elsif ($line =~ /Average Power:\s+(\d+) Watts/) { $status{power} = $1; } elsif ($line =~ /True RMS Current:\s+(\d+\.\d+) Amps/) { $status{current} = $1; } # note the /g, controllers with 20 ports put two outlets per line elsif (my %ohash = $line =~ /Outlet\s+(\d+)\s+:\s+(On|Off)/g) { for my $o (keys(%ohash)) { my $outlet = "outlet$o"; $status{$outlet} = $ohash{$o}; } } } %$statusp = %status; print "Returning status\n" if ($debug); } return 0; } # # Connect up to the capture process. This should probably be in a library # someplace. # sub tipconnect($) { my($controller) = $_[0]; my($server, $portnum, $keylen, $keydata, $capreturn); my($inetaddr, $paddr, $proto); my(%powerid_row); local *TIP; my $query_result = DBQueryWarn("select * from tiplines where node_id='$controller'"); if ($query_result->numrows < 1) { print STDERR "*** No such tipline: $controller\n"; return 0; } %powerid_row = $query_result->fetchhash(); $server = $powerid_row{'server'}; $portnum = $powerid_row{'portnum'}; $keylen = $powerid_row{'keylen'}; $keydata = $powerid_row{'keydata'}; if ($debug) { print "tipconnect: $server $portnum $keylen $keydata\n"; } # # We have to send over the key. This is a little hokey, since we have # to make it look like the C struct. # my $secretkey = pack("iZ256", $keylen, $keydata); my $capret = pack("i", 0); # # This stuff from the PERLIPC manpage. # if (! ($inetaddr = inet_aton($server))) { print STDERR "*** Cannot map $server to IP address\n"; return 0; } $paddr = sockaddr_in($portnum, $inetaddr); $proto = getprotobyname('tcp'); for (my $i = 0; $i < 20; $i++) { if (! socket(TIP, PF_INET, SOCK_STREAM, $proto)) { print STDERR "*** Cannot create socket.\n"; return 0; } if (! connect(TIP, $paddr)) { print STDERR "*** Cannot connect to $controller on $server($portnum)\n"; close(TIP); return 0; } TIP->autoflush(1); # # While its a fatal error if the connect fails, the write and the # read that follows might fail because the tip is currently is # active. The handshake writes back a value and then immediately # closes the socket, which could manifest itself as a closed # connection on this end, even before we get a change to do these. # operations. In that case, just go around the loop again. We hope # to succeed at some point. # if (! syswrite(TIP, $secretkey)) { print STDERR "*** Cannot write to $controller on $server($portnum)\n"; goto again; } if (! sysread(TIP, $capret, length($capret))) { print STDERR "*** Cannot read from $controller on $server($portnum)\n"; goto again; } my $foo = unpack("i", $capret); if ($debug) { print "Capture returned $foo\n"; } if ($foo == 0) { return(*TIP); } again: close(TIP); if ($i && (($i % 5) == 0)) { printf STDERR "*** WARNING: $controller on $server($portnum) is busy\n". " Waiting a bit before trying again. Pass $i.\n"; } sleep(5); } print STDERR "*** $controller on $server($portnum) was busy for too long\n"; return 0; } sub rpc_readline($) { my ($TIP) = @_; my $line; my $cc = 0; while (1) { if (sysread($TIP, $line, 1, $cc) == 0) { return undef; } $cc++; last if ($line =~ /\n/ || $cc > 1023); } return $line; } 1;