reserve.in 20.3 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 NodeType;
48
use libEmulab;
49 50 51

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

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

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

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

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

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

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

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

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

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

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

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

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

    my $query_result = DBQueryFatal( $query );

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

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

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

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

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

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

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

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

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

    my $query_result = DBQueryFatal( $query );

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

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

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

#
# 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;
	}
473 474 475 476 477 478
	if ($abortcancel) {
	    $res->ClearCancel();
	}
	else {
	    $res->MarkCancel($cancel);
	}
Leigh Stoller's avatar
Leigh Stoller committed
479 480 481
	Reservation->EndTransaction();
	last;
    }
482
    
Leigh Stoller's avatar
Leigh Stoller committed
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 508
    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 );
}

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

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

545 546
# For now, auto-approve reservation requests up to some number node-hours
# as defined by a nifty site variable.
547
# Later we'll probably want this threshold to vary based on the node type,
548 549
# how far into the future the reservation starts, existing approved
# reservations for the same project, the phase of the moon...
550
# who knows.
551
#
552
# If they said "-p", don't approve no matter what.
553 554
# If an admin said to force approval, that overrides $pending and limits.
#
555
if (! ($pending || $approve)) {
556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579
    my $approval_threshold;
    if (! GetSiteVar("reservations/approval_threshold", \$approval_threshold)) {
	$approval_threshold = 128;
    }
    if ($approval_threshold == 0 ||
	($count * ( $endtime - $starttime ) / 3600 <= $approval_threshold)) {
	$approve = 1;
    }
    #
    # Check for an override attribute on the node type. Here zero really means
    # zero instead of unlimited. As above, this limit is node hours.
    #
    my $nodetype = NodeType->Lookup($type);
    if (!defined($nodetype)) {
	fatal("Could not look up node type");
    }
    if (defined($nodetype->GetAttribute("reservation_autoapprove_limit"))) {
	my $limit = $nodetype->GetAttribute("reservation_autoapprove_limit");

	if (($count * ( $endtime - $starttime ) / 3600) > $limit) {
	    $approve = 0;
	}
    }
}
580

581 582 583 584 585 586
#
# Do not allow this as root; we want proper history.
#
if ($UID == 0) {
    fatal("Please do not run this as root!");
}
587 588
my $uid = $target_user->uid();
my $uid_idx = $target_user->uid_idx();
589

590 591 592
my $res;
if( defined( $modify_idx ) ) {
    $res = Reservation->Lookup( $modify_idx );
593 594 595 596 597 598 599 600
    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 );
    }
601 602 603 604
    # Okay, user is shrinking a reservation, always leave that in same state.
    if (!$admin) {
	$approve = $res->approved() ? 1 : 0;
    }
605 606 607
    $res->SetStart( $starttime );
    $res->SetEnd( $endtime );
    $res->SetNodes( $count );
608 609 610 611
    # 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();
612 613 614 615
} else {
    $res = Reservation->Create( $pid, $uid, $starttime, $endtime, $type,
				$count );
}
616 617
$res->SetNotes( $notes ) if( defined( $notes ) );
$res->SetAdminNotes( $adminnotes ) if( defined( $adminnotes ) );
618
$res->Approve( $target_user ) if( $approve );
619 620 621 622 623 624

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

while( 1 ) {
    my $version = Reservation->GetVersion();
    my $reservations = Reservation->LookupAll( $type );
625
    my $found = 0;
626 627 628 629 630 631 632 633
    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;
634
		$found = 1;
635 636 637
		last;
	    }
	}
638 639 640 641 642 643

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