reserve.in 19 KB
Newer Older
1 2
#!/usr/bin/perl -w
#
3
# Copyright (c) 2016-2018 University of Utah and the Flux Group.
4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
# 
# {{{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 Getopt::Std;
use Date::Parse;
28
use POSIX;
29 30 31 32 33 34 35 36 37 38 39 40 41 42 43

#
# Configure variables
#
my $TB		 = "@prefix@";
my $TBOPS        = "@TBOPSEMAIL@";

#
# Testbed Support libraries
#
use lib "@prefix@/lib";
use emdb;
use libtestbed;
use Project;
use Reservation;
44
use WebTask;
Leigh B Stoller's avatar
Leigh B Stoller committed
45
use emutil;
46 47 48

sub usage()
{
49
    print STDERR "Usage: reserve [-C] [-f] [-n] [-q] -t type [-s start] [-e end]\n" .
50
	"            [-u] [-U uid] [-N file] [-A file] [-a|-p] pid count\n";
51
    print STDERR "       reserve [-D file] -c idx\n";
52
    print STDERR "       reserve [-f] [-n] [-s start] [-e end] [-u]\n" .
Gary Wong's avatar
Gary Wong committed
53
	"            [-U uid] [-N file] [-A file] [-S size] [-a] -m idx \n";
54 55
    print STDERR "       reserve [-u] -i pid\n";
    print STDERR "       reserve [-u] -l\n";
56
    print STDERR "   -h   This message\n";
57
    print STDERR "   -u   Interpret/display all times in UTC\n";
58
    print STDERR "   -c   Clear existing reservation (by id)\n";
59
    print STDERR "   -C   Clear existing reservation for project (by date)\n";
60 61 62
    print STDERR "   -f   Force reservation into schedule, even if " .
	"overcommitted\n";
    print STDERR "   -n   Check feasibility only; don't actually reserve\n";
63
    print STDERR "   -q   Quiet operation; don't e-mail user\n";
64
    print STDERR "   -U   Mark reservation as being created by uid (admin-only)\n";
65 66 67 68 69
    print STDERR "   -t   Node type\n";
    print STDERR "   -i   Show existing reservation for project\n";
    print STDERR "   -l   List all existing reservations\n";
    print STDERR "   -s   Start time when reservation begins\n";
    print STDERR "   -e   End time when reservation expires\n";
Leigh B Stoller's avatar
Leigh B Stoller committed
70 71
    print STDERR "   -E   Schedule reservation for cancellation\n";
    print STDERR "   -O   Clear scheduled reservation cancellation\n";
Gary Wong's avatar
Gary Wong committed
72
    print STDERR "   -a   Approve reservation (auto for small, otherwise admin-only)\n";
73
    print STDERR "   -p   Create pending reservation (do not auto-approve)\n";
74 75
    print STDERR "   -m   Modify existing reservation\n";
    print STDERR "   -S   Specify new size of modified reservation\n";
76 77 78
    print STDERR "   -A   Supply file containing admin-only notes about reservation\n";
    print STDERR "   -N   Supply file containing user notes justifying reservation\n";
    print STDERR "   -D   Supply file containing reason why reservation was denied\n";
79
    exit( -1 );
80 81
}

Leigh B Stoller's avatar
Leigh B Stoller committed
82
my $optlist    = "ac:de:fhilm:npqs:t:uA:CD:N:S:U:T:E:Oy";
83 84 85 86 87 88 89
my $debug      = 0;
my $info       = 0;
my $list       = 0;
my $clear      = 0;
my $clear_idx  = undef;
my $force      = 0;
my $impotent   = 0;
90
my $quiet      = 0;
91
my $modify_idx = undef;
92 93 94 95
my $starttime  = time; # default to starting immediately
my $endtime    = time + 24 * 60 * 60; # default to ending tomorrow
my $notes      = undef;
my $adminnotes = undef;
96
my $denynotes  = undef;
Gary Wong's avatar
Gary Wong committed
97
my $approve    = 0;
98
my $pending    = 0;
Leigh B Stoller's avatar
Leigh B Stoller committed
99 100 101
my $tidy       = 0;
my $cancel;
my $abortcancel= 0;
102 103 104 105
my $type;
my $pid;
my $count;
my $project;
106
my $webtask;
107
my $admin;
108 109 110 111 112 113 114 115 116 117 118 119 120
my $target_user;

sub fatal($)
{
    my ($mesg) = $_[0];

    if (defined($webtask)) {
	$webtask->Exited(-1);
	$webtask->output($mesg);
    }
    die("*** $0:\n".
	"    $mesg\n");
}
121

122 123 124 125 126 127 128 129 130
sub readfile($) {
    local $/ = undef;
    my ($filename) = @_;
    open( FILE, $filename ) or die "$filename: $!";
    my $contents = <FILE>;
    close( FILE );
    return $contents;
}

131 132 133 134 135 136
sub convert($) {
    my ($unixtime) = @_;

    return strftime( "%Y-%m-%d %H:%M", localtime( $unixtime ) );
}

137 138 139 140 141 142 143 144 145 146
#
# Turn off line buffering on output
#
$| = 1;

#
# Untaint the path
# 
$ENV{'PATH'} = "/bin:/sbin:/usr/bin:";

147 148 149 150 151 152 153 154 155 156 157 158 159
#
# Verify user.
#
my $this_user;
if ($UID) {
    $this_user = User->ThisUser();
    if (! defined($this_user)) {
	fatal("You ($UID) do not exist!");
    }
    $admin = $this_user->IsAdmin();
}
$target_user = $this_user;

160 161 162 163 164 165 166 167
#
# Parse command arguments. Once we return from getopts, all that should be
# left are the required arguments.
#
my %options = ();
if (! getopts($optlist, \%options)) {
    usage();
}
168 169 170 171
if (defined($options{"u"})) {
    # handle this option ASAP, since it affects parsing of other options!
    $ENV{ "TZ" } = "UTC";
}
172 173 174 175
if (defined($options{h})) {
    usage();
}
if (defined($options{c})) {
176 177 178 179 180
    $clear_idx = $options{c};
    unless( $clear_idx =~ /^[0-9]+$/ ) {
	fatal( "Invalid reservation index." );
    }
}
181 182 183 184 185 186
if (defined($options{m})) {
    $modify_idx = $options{m};
    unless( $modify_idx =~ /^[0-9]+$/ ) {
	fatal( "Invalid reservation index." );
    }
}
187
if (defined($options{C})) {
188 189 190 191 192 193
    $clear = 1;
}
if (defined($options{d})) {
    $debug = 1;
}
if (defined($options{f})) {
194
    fatal( "-f option requires administrator privileges" ) unless( $admin );
195 196 197 198 199
    $force = 1;
}
if (defined($options{n})) {
    $impotent = 1;
}
Leigh B Stoller's avatar
Leigh B Stoller committed
200
if (defined($options{y})) {
Leigh B Stoller's avatar
Leigh B Stoller committed
201 202
    $tidy = 1;
}
203 204 205
if (defined($options{q})) {
    $quiet = 1;
}
206 207 208 209 210 211 212 213 214 215
if (defined($options{t})) {
    $type = $options{t};
    unless( $type =~ /^[-\w]+$/ ) {
	fatal( "Invalid node type." );
    }
}
if (defined($options{i})) {
    $info = 1;
}
if (defined($options{l})) {
216
    fatal( "-l option requires administrator privileges" ) unless( $admin );
217 218
    $list = 1;
}
219 220 221 222 223 224 225
if (defined($options{T})) {
    $webtask = WebTask->Lookup($options{T});
    if (!defined($webtask)) {
	fatal("No such webtask: " . $options{T});
    }
    $webtask->AutoStore(1);
}
226
if (defined($options{"e"})) {
227 228 229 230 231 232
    $endtime = $options{"e"};
    if ($endtime !~ /^\d+$/) {
	$endtime = str2time($endtime);
	if( !defined( $endtime ) ) {
	    fatal("Could not parse -e option.");
	}
233 234 235
    }
}
if (defined($options{"s"})) {
236 237 238 239 240 241
    $starttime = $options{"s"};
    if ($starttime !~ /^\d+$/) {
	$starttime = str2time($starttime);
	if( !defined( $starttime ) ) {
	    fatal("Could not parse -s option.");
	}
242 243
    }
}
244 245 246 247
if (defined($options{"N"})) {
    $notes = readfile( $options{"N"} );
}
if (defined($options{"A"})) {
248
    fatal( "-A option requires administrator privileges" ) unless( $admin );
249 250
    $adminnotes = readfile( $options{"A"} );
}
251 252 253
if (defined($options{"D"})) {
    $denynotes = readfile( $options{"D"} );
}
254
if (defined($options{"U"})) {
255
    fatal( "-U option requires administrator privileges" ) unless( $admin );
256 257 258 259
    $target_user = User->Lookup($options{"U"});
    fatal("No such user")
	if (!defined($target_user));
}
260 261 262 263 264 265
if (defined($options{S})) {
    $count = $options{S};
    unless( $count =~ /^[0-9]+$/ ) {
	fatal( "Invalid reservation size." );
    }
}
Gary Wong's avatar
Gary Wong committed
266
if (defined($options{'a'})) {
267
    fatal( "-a option requires administrator privileges" ) unless( $admin );
Gary Wong's avatar
Gary Wong committed
268 269
    $approve = 1;
}
270 271 272
if (defined($options{'p'})) {
    $pending = 1;
}
Leigh B Stoller's avatar
Leigh B Stoller committed
273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291
if (defined($options{'E'})) {
    fatal( "-E option requires administrator privileges" ) unless( $admin );
    $cancel = $options{'E'};
    if ($cancel !~ /^\d+$/) {
	$cancel = str2time($cancel);
	if (!defined($cancel)) {
	    fatal("Could not parse -E option.");
	}
    }
}
elsif (defined($options{'O'})) {
    fatal( "-O option requires administrator privileges" ) unless( $admin );
    $abortcancel = 1;
}
if ($tidy) {
    usage() if( @ARGV );
    Reservation->Tidy();
    exit(0);
}
292 293 294 295 296 297 298 299
if ($info) {
    usage() if( @ARGV != 1 );
    
    $pid = $ARGV[0];
}
elsif ($list) {
    usage() if(@ARGV);
}
300 301 302
elsif( defined( $clear_idx ) ) {
    usage() if(@ARGV);
}
303
else {
304 305 306 307 308 309 310
    if( defined( $modify_idx ) ) {
	usage() if( @ARGV || defined( $type ) );
	
	my $oldres = Reservation->Lookup( $modify_idx );
	if( !defined( $oldres ) ) {
	    fatal( "Could not find existing reservation." );
	}
311

312 313 314
	$pid = $oldres->pid();
	$type = $oldres->type();
	$count = $oldres->nodes() unless( defined( $count ) );
315 316
	$starttime = $oldres->start() unless( defined( $options{"s"} ) );
	$endtime = $oldres->end() unless( defined( $options{"e"} ) );
317
    } else {
318
	usage() if( @ARGV != 2 || !defined( $type ) );
319 320 321 322 323
	
	$pid     = shift(@ARGV);
	$count   = shift(@ARGV);
    }
    
324 325 326 327 328 329 330 331
    if( $count < 1 ) {
	fatal( "Must reserve at least one node." );
    }
    
    if( $endtime <= $starttime ) {
	fatal( "Reservation must not end until after it starts." );
    }

332
    if( $endtime <= time && !$clear ) {
333 334 335 336 337 338 339 340 341 342 343 344
	fatal( "Reservation end time has already passed." );
    }

    if( $endtime > time + 3 * 365 * 24 * 60 * 60 ) {
	fatal( "Reservation ends too far in the future." );
    }
}

#
# List all pending reservations.
#
if ($list) {
Gary Wong's avatar
Gary Wong committed
345
    my $query = $type ? "SELECT idx, pid, nodes, type, approved, " .
346 347
	"UNIX_TIMESTAMP( start ) AS s, UNIX_TIMESTAMP( end ) AS e FROM " .
	"future_reservations WHERE type='$type' ORDER BY s" :
Gary Wong's avatar
Gary Wong committed
348
	"SELECT idx, pid, nodes, type, approved, UNIX_TIMESTAMP( start ) AS s, " .
349 350
	"UNIX_TIMESTAMP( end ) AS e FROM future_reservations " .
	"ORDER BY s";
351 352 353 354

    my $query_result = DBQueryFatal( $query );

    if( $query_result->numrows ) {
Gary Wong's avatar
Gary Wong committed
355 356
	print "A Index Start            End              Project             Nodes Type\n";
	print "- ----- -----            ---              -------             ----- ----\n";
357 358 359
    }

    while( my $row = $query_result->fetchrow_hashref() ) {
360
	my $idx = $row->{'idx'};
361 362 363
	my $pid = $row->{'pid'};
	my $nodes = $row->{'nodes'};
	my $type = $row->{'type'};
364 365
	my $start = convert( $row->{'s'} );
	my $end = convert( $row->{'e'} );
Gary Wong's avatar
Gary Wong committed
366
	my $approved = defined( $row->{'approved'} ) ? "Y" : " ";
367

Gary Wong's avatar
Gary Wong committed
368
	printf( "%1s %5d %16s %16s %-19s %5d %s\n", $approved, $idx, $start, $end, $pid, $nodes, $type );
369 370 371 372 373
    }
    
    exit(0);
}

374
my $pid_idx;
375 376 377 378 379 380 381 382 383
if( defined( $clear_idx ) ) {
    my $res = Reservation->Lookup( $clear_idx );
    fatal( "could not find existing reservation" ) unless( defined( $res ) );
    $pid_idx = $res->pid_idx();
    $project = Project->Lookup( $pid_idx );
    if (!defined($project)) {
	fatal("No such project $pid\n");
    }
} else {
384 385
    if ($pid =~ /^(.*):(.*)$/) {
	require GeniHRN;
386

387
	my $urn = GeniHRN::Generate($pid, "authority", "sa");
388

389 390 391 392 393
	$project = Project->LookupNonLocal($urn);
	if (!defined($project)) {
	    fatal("No such nonlocal project $pid\n");
	}
	$pid = $project->pid();
394
    }
395 396
    else {
	$project = Project->Lookup($pid);
397

398 399 400
	if (!defined($project)) {
	    fatal("No such project $pid\n");
	}
401
    }
402
    $pid_idx = $project->pid_idx();
403 404
}

405 406 407 408 409
if( !$admin ) {
    fatal( "You are not a project member" )
	unless( $project->LookupUser( $this_user ) );
}

410 411 412 413
#
# Show and exit.
#
if ($info) {
Gary Wong's avatar
Gary Wong committed
414
    my $query = $type ? "SELECT uid, nodes, type, approved, " .
415
	"UNIX_TIMESTAMP( start ) AS s, UNIX_TIMESTAMP( end ) AS e FROM " .
416
	"future_reservations WHERE type='$type' AND pid_idx=$pid_idx " .
Gary Wong's avatar
Gary Wong committed
417
	"ORDER BY s" : "SELECT uid, nodes, type, approved, " .
418 419
	"UNIX_TIMESTAMP( start ) AS s, UNIX_TIMESTAMP( end ) AS e FROM " .
	"future_reservations WHERE pid_idx=$pid_idx ORDER BY s";
420 421 422 423

    my $query_result = DBQueryFatal( $query );

    if( $query_result->numrows ) {
Gary Wong's avatar
Gary Wong committed
424 425
	print "A Start            End              User                Nodes Type\n";
	print "- -----            ---              ----                ----- ----\n";
426 427 428 429 430 431
    }

    while( my $row = $query_result->fetchrow_hashref() ) {
	my $uid = $row->{'uid'};
	my $nodes = $row->{'nodes'};
	my $type = $row->{'type'};
432 433
	my $start = convert( $row->{'s'} );
	my $end = convert( $row->{'e'} );
Gary Wong's avatar
Gary Wong committed
434
	my $approved = defined( $row->{'approved'} ) ? "Y" : " ";
435

Gary Wong's avatar
Gary Wong committed
436
	printf( "%1s %16s %16s %-19s %5d %s\n", $approved, $start, $end, $uid, $nodes, $type );
437 438 439 440
    }
    
    exit(0);
}
Leigh B Stoller's avatar
Leigh B Stoller committed
441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470

#
# Schedule cancellation and exit.
#
if ($cancel || $abortcancel) {
    my $res;

    if( $modify_idx ) {
	$res = Reservation->Lookup( $modify_idx );
    } else {
	$res = Reservation->Lookup( $pid, $starttime, $endtime, $type, $count );
    }
    
    if( !defined( $res ) ) {
	print STDERR "-E or -O option: no matching reservation found.\n";
	
	exit( 1 );
    }

    $res->SetCancel(($abortcancel ? undef : $cancel));

    while (1) {
	if (!defined(Reservation->BeginTransaction(Reservation->GetVersion()))) {
	    sleep(1);
	    next;
	}
	$res->Book($res->idx());
	Reservation->EndTransaction();
	last;
    }
471
    
Leigh B Stoller's avatar
Leigh B Stoller committed
472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497
    my $user = User->Lookup( $res->uid() );
    my $count = $res->nodes();
    my $type = $res->type();
    my $s = TBDateStringUTC( $res->start() );
    my $e = TBDateStringUTC( $res->end() );
    my $d = TBDateStringUTC( $cancel );
    my $msg = "Your reservation request for $count $type nodes,\n" .
	"starting at $s and ending at $e\n";
    my $subject;

    if ($abortcancel) {
	$subject = "Reservation cancellation has been rescinded";
	$msg .= "is no longer scheduled for cancellation.\n";
    }
    else {
	$subject = "Reservation scheduled for cancellation";
	$msg .= "has been scheduled for cancellation at $d\n" .
	    ( defined( $denynotes ) ?
	      "The reason for cancellation is:\n\n" .
	      $denynotes . "\n" : "" ),
    }
    SENDMAIL($user->email(), $subject, $msg, $TBOPS) unless( $quiet );
	
    exit( 0 );
}

498 499 500
#
# Clear and exit.
#
501 502
if ($clear || $clear_idx) {
    my $res;
503

504 505 506 507 508 509
    if( $clear_idx ) {
	$res = Reservation->Lookup( $clear_idx );
    } else {
	$res = Reservation->Lookup( $pid, $starttime, $endtime, $type, $count );
    }
    
510 511 512 513 514 515 516 517
    if( !defined( $res ) ) {
	print STDERR "reserve: no matching reservation found.\n";
	
	exit( 1 );
    }
    
    $res->Cancel();
    
518 519 520 521 522 523 524 525 526 527 528 529 530
    my $user = User->Lookup( $res->uid() );
    my $count = $res->nodes();
    my $type = $res->type();
    my $s = convert( $res->start() );
    my $e = convert( $res->end() );
    SENDMAIL( $user->email(), "Reservation CANCELLED",
	      "Your reservation request for $count $type nodes,\n" .
	      "starting at $s and ending at\n" .
	      "$e, has been CANCELLED.\n" .
	      ( defined( $denynotes ) ?
		"The reason for cancellation is:\n" .
		$denynotes . "\n" : "" ) ) unless( $quiet );
	
531 532 533
    exit( 0 );
}

Gary Wong's avatar
Gary Wong committed
534 535
# For now, auto-approve reservation requests up to 64 node-hours.
# Later we'll probably want this threshold to vary based on the node type,
536 537
# how far into the future the reservation starts, existing approved
# reservations for the same project, the phase of the moon...
Gary Wong's avatar
Gary Wong committed
538
# who knows.
539
if( $count * ( $endtime - $starttime ) / 3600 <= 128 ) {
Gary Wong's avatar
Gary Wong committed
540 541 542
    $approve = 1;
}

543 544 545
# If they said "-p", don't approve no matter what.
$approve = 0 if( $pending );

546 547 548 549 550 551
#
# Do not allow this as root; we want proper history.
#
if ($UID == 0) {
    fatal("Please do not run this as root!");
}
552 553
my $uid = $target_user->uid();
my $uid_idx = $target_user->uid_idx();
554

555 556 557
my $res;
if( defined( $modify_idx ) ) {
    $res = Reservation->Lookup( $modify_idx );
558 559 560 561 562 563 564 565
    if( !$admin && $res->approved() && ( $starttime < $res->start ||
					 $endtime > $res->end ||
					 $count > $res->nodes ) ) {
	print STDERR "This reservation has already been approved; you\n";
	print STDERR "may no longer expand it.\n";
	    
	exit( 1 );
    }
566 567 568 569
    # Okay, user is shrinking a reservation, always leave that in same state.
    if (!$admin) {
	$approve = $res->approved() ? 1 : 0;
    }
570 571 572
    $res->SetStart( $starttime );
    $res->SetEnd( $endtime );
    $res->SetNodes( $count );
573 574 575 576
    # The user who originally requested the reservation is not necessarily
    # the same one who's modifying it now.
    $uid = $res->uid();
    $uid_idx = $res->uid();
577 578 579 580
} else {
    $res = Reservation->Create( $pid, $uid, $starttime, $endtime, $type,
				$count );
}
581 582
$res->SetNotes( $notes ) if( defined( $notes ) );
$res->SetAdminNotes( $adminnotes ) if( defined( $adminnotes ) );
Gary Wong's avatar
Gary Wong committed
583
$res->Approve( $target_user ) if( $approve );
584 585 586 587 588 589

print "$res\n" if( $debug );

while( 1 ) {
    my $version = Reservation->GetVersion();
    my $reservations = Reservation->LookupAll( $type );
590
    my $found = 0;
591 592 593 594 595 596 597 598
    if( defined( $modify_idx ) ) {
	my $i;

	for( $i = 0; $i < @$reservations; $i++ ) {
	    my $r = $$reservations[ $i ];

	    if( defined( $r->idx() ) && $r->idx() == $modify_idx ) {
		$$reservations[ $i ] = $res;
599
		$found = 1;
600 601 602
		last;
	    }
	}
603 604 605 606 607 608

	if( !$found ) {
	    # Couldn't find existing reservation in LookupAll() results:
	    # probably because it wasn't previously approved.
	    push( @$reservations, $res );	    
	}
609 610 611
    } else {
	push( @$reservations, $res );
    }
612 613 614 615 616 617
    my $error;
    if( !Reservation->IsFeasible( $reservations, \$error ) ) {
	print STDERR "reserve: $error\n";
	if( $force ) {
	    print STDERR "Continuing anyway!\n";
	} else {
618 619 620 621
	    if (defined($webtask)) {
		$webtask->Exited(1);
		$webtask->output($error);
	    }
622 623 624
	    exit( 1 );
	}
    }
625
    exit( $approve ? 0 : 2 ) if( $impotent );
626 627 628
    # FIXME if $modify_idx is set, the old reservation was approved,
    # and $approve is false, then things get ugly.  e-mail the
    # admins and leave the database untouched???
629
    next if( !defined( Reservation->BeginTransaction( $version ) ) );
630
    $res->Book( $modify_idx );
631
    Reservation->EndTransaction();
632 633
    my $s = convert( $starttime );
    my $e = convert( $endtime );
Gary Wong's avatar
Gary Wong committed
634
    if( $approve ) {
635 636 637 638
	# The reservation is approved -- presumably it is either newly
	# approved or edited since first approval.  E-mail the user
	# unconditionally, since it's probably good for them to hear
	# either way.
639 640
	my $user = User->Lookup( $uid );
	SENDMAIL( $user->email(), "Reservation approved",
641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659
		  "Your reservation request for $count $type nodes,\n" .
		  "starting at $s and ending at\n" .
		  "$e, has been approved.\n" .
		  "\n" .
		  "If you do not intend to use these resources, please\n" .
		  "cancel this reservation as soon as possible, since\n" .
		  "the nodes are currently unavailable to other users for\n" .
		  "the duration of your reservation.\n" .
		  "\n" .
		  "Please note that we make no guarantees about the\n" .
		  "availability or suitability of these nodes for your\n" .
		  "experiment(s).\n" .
		  "\n" .
		  "PLEASE NOTE: Reservations are an experimental\n" .
		  "testbed feature under active development.  Until\n" .
		  "further notice, you should expect reservation\n" .
		  "system failures.  Please send reports about the\n" .
		  "reservation system to $TBOPS.\n" .
		  "Thank you for your assistance in debugging this\n" .
660
		  "feature!\n" ) unless( $quiet );
661 662 663 664
	if (defined($webtask)) {
	    $webtask->Exited(0);
	    $webtask->reservation($res->idx());
	}
Gary Wong's avatar
Gary Wong committed
665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681
	exit( 0 );
    } else {
	# We just booked a reservation we didn't pre-approve.  It requires
	# admin attention to be made effective.
	my $idx = $res->idx();

	print STDERR "reserve: reservation is feasible but has NOT yet been approved.\n";
	
	SENDMAIL( $TBOPS, "Reservation request pending",
		  "User \"$uid\" has requested a reservation for $count $type nodes,\n" .
		  "starting at $s and ending at $e.\n" .
		  "\n" .
		  "The request was feasible at the time it was made, but administrator\n" .
		  "approval is required to hold the resources.\n" .
		  "\n" .
		  "You can approve the request by invoking:\n" .
		  "    reserve -a -m $idx\n" .
682
		  "on boss.\n" ) unless( $quiet );
Gary Wong's avatar
Gary Wong committed
683
	
684 685 686 687
	if (defined($webtask)) {
	    $webtask->Exited(2);
	    $webtask->reservation($res->idx());
	}
Gary Wong's avatar
Gary Wong committed
688 689
	exit( 2 );
    }
690
}