reserve.in 20.4 KB
Newer Older
1 2
#!/usr/bin/perl -w
#
3
# Copyright (c) 2016-2019 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 Node;
48
use NodeType;
49
use libEmulab;
50 51 52

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

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

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

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

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

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

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

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

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

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

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

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

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

    my $query_result = DBQueryFatal( $query );

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

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

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

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

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

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

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

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

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

    my $query_result = DBQueryFatal( $query );

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

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

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

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

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

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

546 547
# For now, auto-approve reservation requests up to some number node-hours
# as defined by a nifty site variable.
548
# Later we'll probably want this threshold to vary based on the node type,
549 550
# how far into the future the reservation starts, existing approved
# reservations for the same project, the phase of the moon...
551
# who knows.
552
#
553
# If they said "-p", don't approve no matter what.
554 555
# If an admin said to force approval, that overrides $pending and limits.
#
556
if (! ($pending || $approve)) {
557 558 559 560 561 562 563 564 565 566 567 568 569
    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);
570 571 572
    if (defined($nodetype)) {
	if (defined($nodetype->GetAttribute("reservation_autoapprove_limit"))) {
	    my $limit = $nodetype->GetAttribute("reservation_autoapprove_limit");
573

574 575 576 577 578 579 580 581 582 583
	    if (($count * ( $endtime - $starttime ) / 3600) > $limit) {
		$approve = 0;
	    }
	}
    }
    else {
	# Lets see if type is really a node ID.
	my $node = Node->Lookup($type);
	if (!defined($node)) {
	    fatal("Could not look up node type");
584 585 586
	}
    }
}
587

588 589 590 591 592 593
#
# Do not allow this as root; we want proper history.
#
if ($UID == 0) {
    fatal("Please do not run this as root!");
}
594 595
my $uid = $target_user->uid();
my $uid_idx = $target_user->uid_idx();
596

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

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

while( 1 ) {
    my $version = Reservation->GetVersion();
    my $reservations = Reservation->LookupAll( $type );
632
    my $found = 0;
633 634 635 636 637 638 639 640
    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;
641
		$found = 1;
642 643 644
		last;
	    }
	}
645 646 647 648 649 650

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