reserve.in 19.7 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
use Text::Wrap;
30 31 32 33 34 35 36 37 38 39 40 41 42 43 44

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

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

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

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

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

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

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

135 136 137 138 139 140
sub convert($) {
    my ($unixtime) = @_;

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

141 142 143 144 145 146 147 148 149 150
#
# Turn off line buffering on output
#
$| = 1;

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

151 152 153 154 155 156 157 158 159 160 161 162 163
#
# 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;

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

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

339
    if( $endtime <= time && !$clear ) {
340 341 342 343 344 345 346 347 348 349 350 351
	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) {
352
    my $query = $type ? "SELECT idx, pid, nodes, type, approved, " .
353 354
	"UNIX_TIMESTAMP( start ) AS s, UNIX_TIMESTAMP( end ) AS e FROM " .
	"future_reservations WHERE type='$type' ORDER BY s" :
355
	"SELECT idx, pid, nodes, type, approved, UNIX_TIMESTAMP( start ) AS s, " .
356 357
	"UNIX_TIMESTAMP( end ) AS e FROM future_reservations " .
	"ORDER BY s";
358 359 360 361

    my $query_result = DBQueryFatal( $query );

    if( $query_result->numrows ) {
362 363
	print "A Index Start            End              Project             Nodes Type\n";
	print "- ----- -----            ---              -------             ----- ----\n";
364 365 366
    }

    while( my $row = $query_result->fetchrow_hashref() ) {
367
	my $idx = $row->{'idx'};
368 369 370
	my $pid = $row->{'pid'};
	my $nodes = $row->{'nodes'};
	my $type = $row->{'type'};
371 372
	my $start = convert( $row->{'s'} );
	my $end = convert( $row->{'e'} );
373
	my $approved = defined( $row->{'approved'} ) ? "Y" : " ";
374

375
	printf( "%1s %5d %16s %16s %-19s %5d %s\n", $approved, $idx, $start, $end, $pid, $nodes, $type );
376 377 378 379 380
    }
    
    exit(0);
}

381
my $pid_idx;
382 383 384 385 386 387 388 389 390
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 {
391 392
    if ($pid =~ /^(.*):(.*)$/) {
	require GeniHRN;
393

394
	my $urn = GeniHRN::Generate($pid, "authority", "sa");
395

396 397 398 399 400
	$project = Project->LookupNonLocal($urn);
	if (!defined($project)) {
	    fatal("No such nonlocal project $pid\n");
	}
	$pid = $project->pid();
401
    }
402 403
    else {
	$project = Project->Lookup($pid);
404

405 406 407
	if (!defined($project)) {
	    fatal("No such project $pid\n");
	}
408
    }
409
    $pid_idx = $project->pid_idx();
410 411
}

412 413 414 415 416
if( !$admin ) {
    fatal( "You are not a project member" )
	unless( $project->LookupUser( $this_user ) );
}

417 418 419 420
#
# Show and exit.
#
if ($info) {
421
    my $query = $type ? "SELECT uid, nodes, type, approved, " .
422
	"UNIX_TIMESTAMP( start ) AS s, UNIX_TIMESTAMP( end ) AS e FROM " .
423
	"future_reservations WHERE type='$type' AND pid_idx=$pid_idx " .
424
	"ORDER BY s" : "SELECT uid, nodes, type, approved, " .
425 426
	"UNIX_TIMESTAMP( start ) AS s, UNIX_TIMESTAMP( end ) AS e FROM " .
	"future_reservations WHERE pid_idx=$pid_idx ORDER BY s";
427 428 429 430

    my $query_result = DBQueryFatal( $query );

    if( $query_result->numrows ) {
431 432
	print "A Start            End              User                Nodes Type\n";
	print "- -----            ---              ----                ----- ----\n";
433 434 435 436 437 438
    }

    while( my $row = $query_result->fetchrow_hashref() ) {
	my $uid = $row->{'uid'};
	my $nodes = $row->{'nodes'};
	my $type = $row->{'type'};
439 440
	my $start = convert( $row->{'s'} );
	my $end = convert( $row->{'e'} );
441
	my $approved = defined( $row->{'approved'} ) ? "Y" : " ";
442

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

#
# 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 );
    }

    while (1) {
	if (!defined(Reservation->BeginTransaction(Reservation->GetVersion()))) {
	    sleep(1);
	    next;
	}
472 473 474 475 476 477
	if ($abortcancel) {
	    $res->ClearCancel();
	}
	else {
	    $res->MarkCancel($cancel);
	}
Leigh Stoller's avatar
Leigh Stoller committed
478 479 480
	Reservation->EndTransaction();
	last;
    }
481
    
Leigh Stoller's avatar
Leigh Stoller committed
482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507
    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 );
}

508 509 510
#
# Clear and exit.
#
511 512
if ($clear || $clear_idx) {
    my $res;
513

514 515 516 517 518 519
    if( $clear_idx ) {
	$res = Reservation->Lookup( $clear_idx );
    } else {
	$res = Reservation->Lookup( $pid, $starttime, $endtime, $type, $count );
    }
    
520 521 522 523 524 525 526 527
    if( !defined( $res ) ) {
	print STDERR "reserve: no matching reservation found.\n";
	
	exit( 1 );
    }
    
    $res->Cancel();
    
528 529 530 531 532 533 534 535 536 537 538 539 540
    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 );
	
541 542 543
    exit( 0 );
}

544 545
# For now, auto-approve reservation requests up to some number node-hours
# as defined by a nifty site variable.
546
# Later we'll probably want this threshold to vary based on the node type,
547 548
# how far into the future the reservation starts, existing approved
# reservations for the same project, the phase of the moon...
549
# who knows.
550 551 552 553 554 555 556
#
my $approval_threshold;
if (! GetSiteVar("reservations/approval_threshold", \$approval_threshold)) {
    $approval_threshold = 128;
}
if ($approval_threshold == 0 ||
    ($count * ( $endtime - $starttime ) / 3600 <= $approval_threshold)) {
557 558 559
    $approve = 1;
}

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

563 564 565 566 567 568
#
# Do not allow this as root; we want proper history.
#
if ($UID == 0) {
    fatal("Please do not run this as root!");
}
569 570
my $uid = $target_user->uid();
my $uid_idx = $target_user->uid_idx();
571

572 573 574
my $res;
if( defined( $modify_idx ) ) {
    $res = Reservation->Lookup( $modify_idx );
575 576 577 578 579 580 581 582
    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 );
    }
583 584 585 586
    # Okay, user is shrinking a reservation, always leave that in same state.
    if (!$admin) {
	$approve = $res->approved() ? 1 : 0;
    }
587 588 589
    $res->SetStart( $starttime );
    $res->SetEnd( $endtime );
    $res->SetNodes( $count );
590 591 592 593
    # 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();
594 595 596 597
} else {
    $res = Reservation->Create( $pid, $uid, $starttime, $endtime, $type,
				$count );
}
598 599
$res->SetNotes( $notes ) if( defined( $notes ) );
$res->SetAdminNotes( $adminnotes ) if( defined( $adminnotes ) );
600
$res->Approve( $target_user ) if( $approve );
601 602 603 604 605 606

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

while( 1 ) {
    my $version = Reservation->GetVersion();
    my $reservations = Reservation->LookupAll( $type );
607
    my $found = 0;
608 609 610 611 612 613 614 615
    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;
616
		$found = 1;
617 618 619
		last;
	    }
	}
620 621 622 623 624 625

	if( !$found ) {
	    # Couldn't find existing reservation in LookupAll() results:
	    # probably because it wasn't previously approved.
	    push( @$reservations, $res );	    
	}
626 627 628
    } else {
	push( @$reservations, $res );
    }
629 630 631 632 633 634
    my $error;
    if( !Reservation->IsFeasible( $reservations, \$error ) ) {
	print STDERR "reserve: $error\n";
	if( $force ) {
	    print STDERR "Continuing anyway!\n";
	} else {
635 636 637 638
	    if (defined($webtask)) {
		$webtask->Exited(1);
		$webtask->output($error);
	    }
639 640 641
	    exit( 1 );
	}
    }
642
    exit( $approve ? 0 : 2 ) if( $impotent );
643 644 645
    # 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???
646
    next if( !defined( Reservation->BeginTransaction( $version ) ) );
647
    $res->Book( $modify_idx );
648
    Reservation->EndTransaction();
649 650
    my $s = convert( $starttime );
    my $e = convert( $endtime );
651
    if( $approve ) {
652 653
	$Text::Wrap::columns = 60;
	
654 655 656 657
	# 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.
658 659
	my $user = User->Lookup( $uid );
	SENDMAIL( $user->email(), "Reservation approved",
660 661 662 663
		  "Your reservation request for $count $type nodes,\n" .
		  "starting at $s and ending at\n" .
		  "$e, has been approved.\n" .
		  "\n" .
664 665 666 667 668
		  ($approvenotes ?
		   "*****************************************************\n".
		   wrap("", "", "$approvenotes\n") .
		   "*****************************************************\n\n"
		   : "") .
669 670 671 672 673 674 675 676 677 678 679 680 681 682 683
		  "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" .
684
		  "feature!\n" ) unless( $quiet );
685 686 687 688
	if (defined($webtask)) {
	    $webtask->Exited(0);
	    $webtask->reservation($res->idx());
	}
689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705
	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" .
706
		  "on boss.\n" ) unless( $quiet );
707
	
708 709 710 711
	if (defined($webtask)) {
	    $webtask->Exited(2);
	    $webtask->reservation($res->idx());
	}
712 713
	exit( 2 );
    }
714
}