tbadb.in 13.3 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
#!/usr/bin/perl -w

#
# Copyright (c) 2016 University of Utah and the Flux Group.
# 
# {{{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/>.
# 
# }}}
#

use strict;
use English;
use POSIX ":sys_wait_h";
use Getopt::Std;
use Data::Dumper;
31
use IO::Socket::INET;
32

33
use lib "@prefix@/lib";
34
use libjsonrpc;
35
use tbadb_rpc;
36 37
use EmulabConstants;
use User;
38 39 40
use Node;
use Image;

41 42

# Func prototypes
43
sub cmd_setup($@);
44
sub cmd_loadimage($@);
45
sub cmd_forward($@);
46
sub cmd_reboot($;@);
47 48
sub GetRPCPipeHandles($);
sub ConnectRPCHost($);
49 50

# Global variables
51 52 53 54 55
my $TB = "@prefix@";
my $MINHLEN   = 2;
my $MAXHLEN   = 32;
my $MINCMDLEN = 2;
my $MAXCMDLEN = 32;
56
my %RPCPIPES = ();
57
my $TBADB_PROXYCMD = "/usr/testbed/sbin/tbadb_proxy";
58 59 60
my $TBADB_HELLO_TMO      = 10;
my $TBADB_CHECKIMAGE_TMO = 30;
my $TBADB_LOADIMAGE_TMO  = 120;
61
my $TBADB_FORWARD_TMO    = 15;
62
my $TBADB_REBOOT_TMO     = 60;
63
my $CHILD_WAIT_TMO       = 10;
64 65 66
my $SCP = "/usr/bin/scp";

my %DISPATCH = (
67
    'setup'     => \&cmd_setup,
68
    'loadimage' => \&cmd_loadimage,
69
    'forward'   => \&cmd_forward,
70 71 72 73
    'reboot'    => \&cmd_reboot,
);

sub showhelp() {
74
    print "Usage: $0 -n <node_id> <cmd> <cmd_args>\n\n";
75 76
    print "<cmd>:       TBADB command to run (see list below).\n".
	  "<cmd_args>:  set of arguments specific to <cmd>\n";
77
    print "Command list: ". join(", ", keys %DISPATCH) ."\n";
78
    print "Run again listing just a command to get that command's help.\n";
79 80 81 82 83 84 85 86 87 88 89 90
}

#
# Untaint the path
# 
$ENV{'PATH'} = "/bin:/sbin:/usr/bin:";
delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};

#
# We don't want to run this script unless it's the real version.
#
if ($EUID != 0) {
91
    die("$0: Must be setuid! Maybe it's a development version?\n");
92 93 94 95 96 97 98 99 100 101
}

#
# Verify user and get user's DB uid and other info for later.
#
my $this_user = User->ThisUser();
if (! defined($this_user)) {
    die("You ($UID) do not exist!\n");
}

102
# Parse command line switches.
103
my %opts = ();
104
if (!getopts("dhn:",\%opts) || $opts{'h'} || @ARGV < 1) {
105 106 107 108 109 110 111
    showhelp();
    exit 1;
}

my $debug = $opts{'d'} ? 1 : 0;
$libjsonrpc::debug = 1 if $debug;

112 113 114 115
# Untaint node_id argument, if provided.
my $node_id = $opts{'n'} ? $opts{'n'} : "";
if ($node_id) {
    die "$0: malformed node_id argument!\n"
116
	if ($node_id !~ /^([-\w]{$MINHLEN,$MAXHLEN})$/);
117 118 119 120 121 122 123
    $node_id = $1;    
}

# Gather other command line args.
my ($CMD, @ARGS) = @ARGV;

# Untaint command
124 125
die "$0: malformed command!\n"
    if ($CMD !~ /^([-\w]{$MINCMDLEN,$MAXCMDLEN})$/);
126 127 128
$CMD = $1;

die "$0: unknown command: $CMD\n"
129 130
    if (!exists($DISPATCH{$CMD}));

131
# Setup signal handler stuff.
132 133 134
$SIG{CHLD} = \&chldhandler;
$SIG{HUP} = $SIG{TERM} = $SIG{INT} = \&genhandler;

135 136
# Execute!
exit $DISPATCH{$CMD}->($node_id, @ARGS);
137 138


139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159
#
# "setup" is a compound command.  We just separate out the arguments
# and call the respective individual commands.
#
sub cmd_setup($@) {
    my ($node_id, $imagepid, $imagename, $thost, $tport) = @_;

    # Brief check for correct number of arguments.  The individual
    # commands will do a more thorough check.
    die "tbadb::cmd_setup: missing one or more arguments (need: <node_id> <project> <image_name> <target_adb_host> <target_adb_port>)!\n"
	if (!$node_id || !$imagepid || !$imagename || !$thost || !$tport);

    # Individual commands will die() if they fail, so subsequent calls
    # will not happen if prior ones fail.
    cmd_loadimage($node_id, $imagepid, $imagename);
    cmd_forward($node_id, $thost, $tport);

    # Done!
    return 0;
}

160 161 162 163 164 165 166
#
# Given a valid image identifier (name, osid), project (to scope
# image) and node_id, load an image onto a remote device.  Check with
# the remote side to ensure the image is there, and tranfer it first
# if necessary.  The remote end keeps an LRU cache of images.
#
sub cmd_loadimage($@) {
167 168 169
    my ($node_id, $imagepid, $imagename) = @_;

    # Check and untaint arguments.
170
    die "tbadb::cmd_loadimage: missing one or more arguments (need: <project> <image_name>)!\n"
171 172 173 174 175 176 177
	if (!$node_id || !$imagepid || !$imagename);
    die "tbadb::cmd_loadimage: malformed project id!"
	if ($imagepid !~ /^([-\w]{$MINHLEN,$MAXHLEN})$/);
    $imagepid = $1;
    die "tbadb::cmd_loadimage: malformed image id/name!"
	if ($imagename !~ /^([-\w]{$MINHLEN,$MAXHLEN})$/);
    $imagename = $1;
178 179 180 181 182 183 184 185

    # Lookup image and extract some info.
    my $image = Image->Lookup($imagepid, $imagename);
    die "tbadb::cmd_loadimage: No such image descriptor $imagename in project $imagepid!\n"
	if (!defined($image));
    my $imageid = $image->imageid();
    my $imagepath = $image->path();
    $imagename  = $image->imagename(); # strip any version
186 187 188
    my $size  = $image->size();
    my $mtime;
    $image->GetUpdate(\$mtime);
189 190 191 192 193 194 195 196 197 198 199 200 201 202

    # Check user's access to the image.
    die "tbadb::cmd_loadimage: You do not have permission to use imageid $imageid!\n"
	if (!$this_user->IsAdmin() &&
	    !$image->AccessCheck($this_user, TB_IMAGEID_ACCESS));
    die "tbadb::cmd_loadimage: Cannot access image file: $imagepath\n"
	if (!-r $imagepath);

    # Make sure user has access to requested node too.
    my $node = Node->Lookup($node_id);
    die "tbadb::cmd_loadimage: Invalid node name $node_id!\n"
	if (!defined($node));
    die("tbadb::cmd_loadimage: You do not have permission to load an image onto $node\n")
	if (!$node->AccessCheck($this_user, TB_NODEACCESS_LOADIMAGE));
203 204 205 206 207
    
    # Grab the RPC pipe.
    my ($rpcin, $rpcout) = GetRPCPipeHandles($node);
    die "tbadb::cmd_reboot: Failed to get valid SSH pipe filehandles!\n"
	if (!$rpcin || !$rpcout);
208 209 210

    # Have remote side check for this image in its cache.
    die "tbadb::cmd_loadimage: Failed to send 'checkimage' RPC!\n"
211
	if (!SendRPCData($rpcout, 
212 213
			 EncodeCall("checkimage",
				    {
214
					IMG_PROJ => $imagepid,
215 216 217 218 219 220
					IMG_NAME => $imagename,
					IMG_TIME => $mtime,
					IMG_SIZE => $size,
				    })));
    my $pdu;
    die "tbadb::cmd_loadimage: Failed to receive valid response for 'checkimage'\n"
221
	if (RecvRPCData($rpcin, \$pdu, $TBADB_CHECKIMAGE_TMO) != 1);
222 223 224 225 226 227 228 229 230 231 232
    my $data = DecodeRPCData($pdu);
    die "tbadb::cmd_loadimage: Could not decode RPC response from 'checkimage'"
	if (!$data);
    if (exists($data->{ERROR})) {
	warn "tbadb::cmd_loadimage: Received error from 'checkimage':\n";
	warn "". Dumper($data);
	exit 1;
    }

    # Transfer the image to the remote host if necessary (SCP).
    if ($data->{RESULT}->{NEED_IMG} == 1) {
233 234 235 236
	my $rhost;
	$node->TipServer(\$rhost);
	die "tbadb::cmd_loadimage: Could not lookup control server for $node!\n"
	    if (!$rhost);
237 238 239
	die "tbadb::cmd_loadimage: Malformed remote image path!\n"
	    if ($data->{RESULT}->{REMOTE_PATH} !~ /^([-\/\w]+)$/);
	my $rpath = $1;
240
	print "tbadb: Sending $imagepath to $rhost\n";
241 242
	my $SAVEUID = $UID; 
	$EUID = $UID = 0; # Flip to root to run!
243
	die "tbadb::cmd_loadimage: Failed to transfer image to $rhost: $imagepath\n"
244 245
	    if (mysystem($SCP, '-q', '-B', '-p', 
			 "$imagepath", "$rhost:$rpath/$imagename") != 0);
246
	$EUID = $UID = $SAVEUID; # Flip back.
247 248 249 250 251
    }

    # Now that the image is (ostensibly) in place on the remote side,
    # ask the remote host to load it onto the device.
    die "tbadb::cmd_loadimage: Failed to send 'loadimage' RPC!\n"
252
	if (!SendRPCData($rpcout, 
253 254
			 EncodeCall("loadimage",
				    {
255 256
					IMG_PROJ => $imagepid,
					IMG_NAME => $imagename,
257 258 259
					NODE_ID  => $node_id,
				    })));
    die "tbadb::cmd_loadimage: Failed to receive response for 'loadimage'\n"
260
	if (RecvRPCData($rpcin, \$pdu, $TBADB_LOADIMAGE_TMO) != 1);
261 262 263 264 265 266 267 268 269 270 271
    $data = DecodeRPCData($pdu);
    die "tbadb::cmd_loadimage: Could not decode RPC response from 'loadimage'\n"
	if (!$data);
    if (exists($data->{ERROR}) || !exists($data->{RESULT}->{SUCCESS})) {
	warn "tbadb::cmd_loadimage: Received error from 'loadimage':\n";
	warn "". Dumper($data);
	exit 1;
    }

    # Done!
    print "tbadb: Successfully loaded $imagepath to $node_id\n";
272
    return 0;
273 274
}

275 276 277 278 279
#
# Forward ADB to a target host and port.  Must provide a valid device
# node_id, target host, and target port.
#
sub cmd_forward($@) {
280
    my ($node_id, $thost, $tport) = @_;
281 282

    # Check and untaint arguments
283
    die "tbadb::cmd_forward: missing arguments! (need: <target_host> <target_port>\n"
284 285
	if (!$node_id || !$thost || !$tport);
    die "tbadb::cmd_forward: malformed target host!"
286
	if ($thost !~ /^([-\.\w]{$MINHLEN,$MAXHLEN})$/);
287 288
    $thost = $1;
    die "tbadb::cmd_forward: malformed target port!"
289
	if ($tport !~ /^(\d+)$/ || $1 < 1 || $1 > 65535);
290
    $tport = $1;
291

292 293 294 295
    # Make sure user has access to requested node
    my $node = Node->Lookup($node_id);
    die "tbadb::cmd_forward: Invalid node name $node_id!\n"
	if (!defined($node));
296
    die("tbadb::cmd_forward: You do not have permission to access $node\n")
297 298 299 300 301 302 303
	if (!$node->AccessCheck($this_user, TB_NODEACCESS_REBOOT));
    
    # Grab the RPC pipe.
    my ($rpcin, $rpcout) = GetRPCPipeHandles($node);
    die "tbadb::cmd_reboot: Failed to get valid SSH pipe filehandles!\n"
	if (!$rpcin || !$rpcout);
    
304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325
    # Request adb port forwarding on device's control host.
    die "tbadb::cmd_forward: Failed to send 'forward' RPC!\n"
	if (!SendRPCData($rpcout, 
			 EncodeCall("forward", {
			     NODE_ID     => $node_id,
			     TARGET_HOST => $thost,
			     TARGET_PORT => $tport,})));

    # Grab remote result.
    my $pdu;
    die "tbadb::cmd_forward: Failed to receive valid response for 'forward'\n"
	if (RecvRPCData($rpcin, \$pdu, $TBADB_FORWARD_TMO) != 1);
    my $data = DecodeRPCData($pdu);
    die "tbadb::cmd_forward: Could not decode RPC response from 'forward'"
	if (!$data);

    # Check returned result.
    if (exists($data->{ERROR}) || !exists($data->{RESULT}->{SUCCESS})) {
	warn "tbadb::cmd_forward: Received error from 'forward':\n";
	warn "". Dumper($data);
	exit 1;
    }
326 327 328 329
    
    # Done!
    return 0;
}
330

331 332 333
#
# Given a valid node_id, reboot a device.
#
334
sub cmd_reboot($;@) {
335
    my ($node_id) = @_;
336

337 338 339
    # Check and untaint arguments;
    die "tbadb::cmd_reboot: node_id argument missing!\n"
	if (!$node_id);
340 341 342 343 344 345 346 347

    # Make sure user has access to requested node
    my $node = Node->Lookup($node_id);
    die "tbadb::cmd_reboot: Invalid node name $node_id!\n"
	if (!defined($node));
    die("tbadb::cmd_reboot: You do not have permission to reboot $node\n")
	if (!$node->AccessCheck($this_user, TB_NODEACCESS_REBOOT));

348 349
    # Grab the RPC pipe.
    my ($rpcin, $rpcout) = GetRPCPipeHandles($node);
350
    die "tbadb::cmd_reboot: Failed to get valid SSH pipe filehandles!\n"
351
	if (!$rpcin || !$rpcout);
352

353
    # Request device reboot via remote host.
354
    die "tbadb::cmd_reboot: Failed to send 'reboot' RPC!\n"
355
	if (!SendRPCData($rpcout, 
356 357
			 EncodeCall("reboot", { NODE_ID => $node_id })));

358
    # Wait for reboot and grab returned result.
359 360
    my $pdu;
    die "tbadb::cmd_reboot: Failed to receive valid response for 'reboot'\n"
361
	if (RecvRPCData($rpcin, \$pdu, $TBADB_REBOOT_TMO) != 1);
362
    my $data = DecodeRPCData($pdu);
363
    die "tbadb::cmd_reboot: Could not decode RPC response from 'reboot'"
364 365
	if (!$data);

366
    # Check returned result.
367
    if (exists($data->{ERROR}) || !exists($data->{RESULT}->{SUCCESS})) {
368
	warn "tbadb::cmd_reboot: Received error from 'reboot':\n";
369
	warn "". Dumper($data);
370
	exit 1;
371
    }
372 373

    # Done!
374
    print "tbadb: Successfully rebooted $node_id\n";
375
    return 0;
376 377
}

378 379 380 381 382 383
# Helper that returns the RPC in/out pipe pair.  Establishes the remote
# connection if necessary.  Argument is a node object.
sub GetRPCPipeHandles($) {
    my ($node) = @_;
    my ($rpcin, $rpcout);

384 385
    # Look up the node's control (console) server and connect to it if
    # we haven't done so yet.  Otherwise grab and return the open pipe.
386 387 388 389
    my $conserver;
    $node->TipServer(\$conserver);
    die "tbadb::GetRPCPipeHandles: Could not lookup control server for $node!\n"
	if (!$conserver);
390 391
    if (!exists($RPCPIPES{$conserver})) {
	$RPCPIPES{$conserver} = ConnectRPCHost($conserver);
392
    }
393 394 395
    my $rpcpipe = $RPCPIPES{$conserver};
    die "tbadb::GetRPCPipeHandles: RPC pipe for $conserver closed unexpectedly!"
	if (!$rpcpipe->connected());
396

397
    return ($rpcpipe, $rpcpipe);
398 399
}

400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417
# Helper that connects to a remote TBADB RPC proxy service.
sub ConnectRPCHost($) {
    my ($host) = @_;

    # Connect and read in expected "hello" message.
    my $socket = 
	IO::Socket::INET->new(
	    PeerAddr => $host,
	    PeerPort => TBADB_PORT,
	    Proto    => 'tcp'
	);
    die "tbadb::ConnectRPCHost: Could not connect to tbadb proxy on host $host: $!\n"
	if (!$socket);
    $socket->autoflush(1);
    my $pdu;
    my $res = RecvRPCData($socket, \$pdu, $TBADB_HELLO_TMO);
    if ($res == -1) {
	die "tbadb::ConnectRPCHost: Timeout while opening RPC Pipe!\n";
418
    }
419 420
    elsif ($res == 0) {
	die "tbadb::ConnectRPCHost: Error encountered while opening RPC Pipe!\n";
421
    }
422 423 424 425 426 427 428 429
    # Look for the hello.
    my $hello = DecodeRPCData($pdu);
    die "tbadb::ConnectRPCHost: Unexpected data received when opening RPC Pipe!\n"
	if (!$hello);
    die "tbadb::ConnectRPCHost: Did not receive valid 'hello' from remote end!\n"
	if (!exists($hello->{RESULT}->{HELLO}));

    return $socket;
430
}