Commit c7c93e9f authored by Leigh B Stoller's avatar Leigh B Stoller
Browse files

Reservation system changes:

1. Allow uuids to be used to specify reservations, change pretty much
   everything in the web interface to use uuid's so we stop exporting
   databases indexes to the client side.

2. Change RPC path to return a blob of data when approving a
   reservation. Ditto for initial creation, so that we can see precisely
   what the local cluster has done.

3. When a reservation is created/approved, insert an announcement in the
   announce system for that user, set to go off 24 hours ahead of
   reservation. Update that announcement when reservation is modified.
parent 14fba2b4
......@@ -36,8 +36,8 @@ use Date::Parse;
sub usage()
{
print("Usage: manage_reservations [-a <urn>] list [-u uid | -p pid]\n");
print("Usage: manage_reservations [-a <urn>] delete pid idx\n");
print("Usage: manage_reservations [-a <urn>] approve idx\n");
print("Usage: manage_reservations [-a <urn>] delete pid uuid\n");
print("Usage: manage_reservations [-a <urn>] approve -p portal uuid\n");
print("Usage: manage_reservations [-a <urn>] systeminfo\n");
print("Usage: manage_reservations [-a <urn>] prediction\n");
exit(-1);
......@@ -54,6 +54,7 @@ my $authority;
my $TB = "@prefix@";
my $TBOPS = "@TBOPSEMAIL@";
my $OURDOMAIN = "@OURDOMAIN@";
my $ANNOUNCE = "$TB/sbin/announce";
my $MYURN = "urn:publicid:IDN+${OURDOMAIN}+authority+cm";
#
......@@ -96,6 +97,8 @@ sub DoApprove();
sub DoSystemInfo();
sub DoPrediction();
sub readfile($);
sub AddAnnouncement($$$$$$);
sub DeleteAnnouncement($);
#
# Parse command arguments. Once we return from getopts, all that should be
......@@ -134,6 +137,11 @@ else {
if (!defined($authority)) {
fatal("Could not look up authority");
}
my $aggregate = APT_Aggregate->Lookup($authority->urn());
if (!defined($aggregate)) {
fatal("Could not lookup aggregate");
}
# For credentials.
my $this_user = User->ThisUser();
if (! defined($this_user)) {
......@@ -296,9 +304,28 @@ sub DoReserve()
# approval code indicates if the request is feasible and will be
# immediately approved.
#
if ($response->value()) {
# approved right away
exit(0);
my $approved;
my $uuid;
my $idx;
if (ref($response->value()) eq "HASH") {
my $blob = $response->value();
$approved = $blob->{'approved'};
$uuid = $blob->{'uuid'} if (exists($blob->{'uuid'}));
$idx = $blob->{'idx'} if (exists($blob->{'idx'}));
if (defined($idx) && $idx !~ /^\d*$/) {
fatal("Bad format for index from cluster");
}
if (defined($uuid) && !ValidUUID($uuid)) {
fatal("Bad format for uuid from cluster");
}
}
else {
# Backwards compat.
$approved = $response->value();
}
if ($checkonly) {
exit($approved ? 0 : 2);
}
#
# Needs to be approved, exit with special status.
......@@ -307,7 +334,7 @@ sub DoReserve()
# since the email was generated to the tbops but not the portal
# email lists.
#
if (!$checkonly) {
if (!$approved) {
my $this_uid = $this_user->uid();
my $this_email = $this_user->email();
my $url = $brand->wwwBase() . "/list-reservations.php";
......@@ -318,8 +345,18 @@ sub DoReserve()
"\n\n".
"See: $url", $this_email);
}
exit(2);
elsif (defined($uuid)) {
#
# Schedule an announcement for the user.
#
AddAnnouncement($portal, $brand, $uuid, $count, $type, $start);
}
if (defined($webtask)) {
$webtask->approved($approved);
$webtask->idx($idx) if (defined($idx));
$webtask->uuid($uuid) if (defined($uuid));
}
exit($approved ? 0 : 2);
}
#
......@@ -332,7 +369,7 @@ sub DoList()
#
my $optlist = "u:p:Ai:";
my $anon = 0;
my $idx;
my $uuid;
my $project;
my $user;
my %rpcargs = ();
......@@ -346,7 +383,7 @@ sub DoList()
$anon = 1;
}
if (defined($options{"i"})) {
$idx = $options{"i"};
$uuid = $options{"i"};
}
if (defined($options{"u"})) {
$user = User->Lookup($options{"u"});
......@@ -396,8 +433,8 @@ sub DoList()
$rpcargs{"credentials"} = $credentials;
$context = APT_Geni::GeniContext();
}
if (defined($idx)) {
$rpcargs{"idx"} = $idx;
if (defined($uuid)) {
$rpcargs{"uuid"} = $uuid;
}
my $response =
APT_Geni::PortalRPC($authority, $context, "Reservations", (\%rpcargs));
......@@ -492,12 +529,12 @@ sub DoDelete()
}
usage()
if (@ARGV != 2);
my $pid = shift(@ARGV);
my $idx = shift(@ARGV);
my $pid = shift(@ARGV);
my $uuid = shift(@ARGV);
# Check this since Reservation->Lookup() does not validate.
fatal("Invalid index")
if ($idx !~ /^\d+$/);
fatal("Invalid uuid")
if (!ValidUUID($uuid));
my $project = Project->Lookup($pid);
fatal("No such project")
......@@ -507,7 +544,7 @@ sub DoDelete()
!$project->AccessCheck($this_user, TB_PROJECT_CREATEEXPT())) {
fatal("No permission to access reservation list for $project")
}
my $blob = { "idx" => $idx,
my $blob = { "uuid" => $uuid,
"project" => $project->urn()->asString()
};
if ($this_user->IsAdmin() && defined($reason) && $reason ne "") {
......@@ -521,6 +558,7 @@ sub DoDelete()
#
fatal($response->output());
}
DeleteAnnouncement($uuid);
if (defined($webtask)) {
$webtask->Exited(0);
}
......@@ -535,13 +573,28 @@ sub DoDelete()
#
sub DoApprove()
{
my $optlist = "p:";
my $portal;
my %options = ();
if (! getopts($optlist, \%options)) {
usage();
}
if (defined($options{"p"})) {
$portal = $options{"p"}
}
usage()
if (@ARGV != 1);
my $idx = shift(@ARGV);
if (@ARGV != 1 || !defined($portal));
my $uuid = shift(@ARGV);
# Check this since Reservation->Lookup() does not validate.
fatal("Invalid index")
if ($idx !~ /^\d+$/);
fatal("Invalid uuid")
if (!ValidUUID($uuid));
my $brand = Brand->Create($portal);
if (!defined($brand)) {
fatal("Bad branding");
}
if (!$this_user->IsAdmin()) {
fatal("No permission to approve reservations")
......@@ -550,13 +603,20 @@ sub DoApprove()
# essentially saying the caller is an admin.
my $response =
APT_Geni::PortalRPC($authority, undef, "ApproveReservation",
{"idx" => $idx});
{"uuid" => $uuid});
if (GeniResponse::IsError($response)) {
#
# All errors are fatal.
#
fatal($response->output());
}
#
# Schedule an announcement for the user.
#
my $blob = $response->value();
AddAnnouncement($portal, $brand, $uuid,
$blob->{'nodes'}, $blob->{'type'}, $blob->{'start'});
if (defined($webtask)) {
$webtask->Exited(0);
}
......@@ -595,10 +655,10 @@ sub DoSystemInfo()
@aggregates = ();
my @list = APT_Aggregate->LookupForPortal($portal);
foreach my $aggregate (@list) {
my $authority = APT_Geni::GetAuthority($aggregate->urn());
foreach my $agg (@list) {
my $authority = APT_Geni::GetAuthority($agg->urn());
if (!defined($authority)) {
$errmsg = "Cannot lookup authority for $aggregate";
$errmsg = "Cannot lookup authority for $agg";
goto bad;
}
push(@aggregates, $authority);
......@@ -749,10 +809,10 @@ sub DoPrediction()
@aggregates = ();
my @list = APT_Aggregate->LookupForPortal($portal);
foreach my $aggregate (@list) {
my $authority = APT_Geni::GetAuthority($aggregate->urn());
foreach my $agg (@list) {
my $authority = APT_Geni::GetAuthority($agg->urn());
if (!defined($authority)) {
$errmsg = "Cannot lookup authority for $aggregate";
$errmsg = "Cannot lookup authority for $agg";
goto bad;
}
push(@aggregates, $authority);
......@@ -890,3 +950,57 @@ sub readfile($) {
return $contents;
}
#
# Add or Edit a reservation announcement.
#
sub AddAnnouncement($$$$$$)
{
my ($portal, $brand, $uuid, $nodes, $type, $start) = @_;
my $this_uid = $this_user->uid();
my $nickname = $aggregate->nickname();
my $name = $aggregate->name();
my $url = $brand->wwwBase() .
"/reserve.php?edit=1&uuid=${uuid}&cluster=${nickname}";
my $text = "You have a reservation at $name for $nodes $type ".
"node(s) starting soon.";
my $command;
my $dateopt = "";
#
# If the reservation is more then 24 hours out, schedule an announcement
# for 24 hours ahead.
#
if (defined($start)) {
if ($start !~ /^\d+$/) {
$start = str2time($start);
}
if ($start - time() > (3600 * 24)) {
$dateopt = "-S " . ($start - (3600 * 23));
}
$dateopt = "-S " . (time() + 30);
}
# This is awkward, the announcement stuff should be a library some day.
my $query_result =
emdb::DBQueryWarn("select idx from apt_announcements where uuid='$uuid'");
return
if (!$query_result);
# Update mode.
if ($query_result->numrows) {
$command = "$ANNOUNCE -A $uuid -b 'View Reservation' ".
"-u '$url' $dateopt '$text'";
}
else {
$command = "$ANNOUNCE -a -p $portal -U $this_uid -I 1 ".
"-b 'View Reservation' -u '$url' -t $uuid $dateopt '$text'";
}
system($command);
}
sub DeleteAnnouncement($)
{
my ($uuid) = @_;
system("$ANNOUNCE -R $uuid");
}
......@@ -704,6 +704,7 @@ sub Reserve($)
my %blob = ();
my $reserror;
my $asadmin = 0;
my $reservation;
my ($geniuser, $project);
my $hasperm = CheckPermission(0);
......@@ -763,7 +764,8 @@ sub Reserve($)
# Different rules for update.
my $update = (exists($argref->{"update"}) ? $argref->{"update"} : undef);
return GeniResponse->MalformedArgsResponse("Invalid update index")
if (defined($update) && $update !~ /^\d+$/);
if (defined($update) &&
!($update =~ /^\d+$/ || ValidUUID($update)));
#
# Required arguments when not an update.
......@@ -841,7 +843,7 @@ sub Reserve($)
}
if (defined($update)) {
my $reservation = Reservation->Lookup($update);
$reservation = Reservation->Lookup($update);
return GeniResponse->SearchFailedResponse("No such reservation")
if (!defined($reservation));
......@@ -875,7 +877,7 @@ sub Reserve($)
if (!defined($webtask));
my $args = ($check ? "-n " : "") .
(defined($update) ? "-m $update " : "") .
(defined($update) ? "-m " . $reservation->idx() . " " : "") .
"-T " . $webtask->task_id() . " ".
(!defined($update) ? "-t $type " : "") .
(defined($start) ? "-s $start " : "") .
......@@ -918,8 +920,31 @@ sub Reserve($)
}
my $approved = ($? >> 8 == 2 ? 0 : 1);
GeniUtil::FlipToGeniUser();
if ($check) {
$webtask->Delete();
my $blob = {
"approved" => $approved,
};
return GeniResponse->Create(GENIRESPONSE_SUCCESS, $blob);
}
if (!$update) {
$webtask->Refresh();
my $idx = $webtask->reservation();
$reservation = Reservation->Lookup($idx);
if (!defined($reservation)) {
$webtask->Delete();
return GeniResponse->Create(GENIRESPONSE_ERROR, undef,
"Could not find reservation after create");
}
}
$webtask->Delete();
return GeniResponse->Create(GENIRESPONSE_SUCCESS, $approved);
my $blob = {
"idx" => $reservation->idx(),
"uuid" => $reservation->uuid(),
"approved" => $approved,
};
print STDERR Dumper($blob);
return GeniResponse->Create(GENIRESPONSE_SUCCESS, $blob);
}
#
......@@ -931,17 +956,21 @@ sub Reservations($)
my %results = ();
my $reserror;
my $query_result;
my $idx;
my $target;
my $hasperm = CheckPermission(0);
return $hasperm
if (GeniResponse::IsError($hasperm));
if (exists($argref->{'idx'})) {
$idx = $argref->{'idx'};
return GeniResponse->MalformedArgsResponse("Invalid idx")
if ($idx !~ /^\d+$/);
if ($argref->{'idx'} !~ /^\d+$/);
$target = "idx='" . $argref->{'idx'} . "'";
}
elsif (exists($argref->{'uuid'})) {
return GeniResponse->MalformedArgsResponse("Invalid uuid")
if (!ValidUUID($argref->{'uuid'}));
$target = "uuid='" . $argref->{'uuid'} . "'";
}
#
......@@ -963,7 +992,7 @@ sub Reservations($)
" UNIX_TIMESTAMP(end) as end, ".
" UNIX_TIMESTAMP(created) as created ".
" from future_reservations ".
(defined($idx) ? "where idx='$idx' " : "") .
(defined($target) ? "where $target " : "") .
"order BY start");
}
else {
......@@ -981,7 +1010,7 @@ sub Reservations($)
" UNIX_TIMESTAMP(created) as created ".
" from future_reservations ".
"where pid='$pid' ".
(defined($idx) ? "and idx='$idx'" : ""));
(defined($target) ? "and $target" : ""));
}
else {
my $uid = $geniuser->uid();
......@@ -997,13 +1026,14 @@ sub Reservations($)
if (!$query_result);
return GeniResponse->Create(GENIRESPONSE_SEARCHFAILED)
if (defined($idx) && !$query_result->numrows);
if (defined($target) && !$query_result->numrows);
while (my $row = $query_result->fetchrow_hashref()) {
my $blob = {};
my $idx = $row->{'idx'};
my $pid = $row->{'pid'};
my $uid = $row->{'uid'};
my $uuid = $row->{'uuid'};
my $project = Project->Lookup($pid);
if (!defined($project)) {
......@@ -1016,13 +1046,13 @@ sub Reservations($)
next;
}
$blob->{"idx"} = $idx;
$blob->{"uuid"} = $uuid;
$blob->{"project"} = $project->nonlocalurn();
$blob->{"user"} = $user->nonlocalurn();
$blob->{"nodes"} = $row->{'nodes'};
$blob->{"type"} = $row->{'type'};
$blob->{"created"} = TBDateStringGMT($row->{'created'});
$blob->{"start"} = TBDateStringGMT($row->{'start'});
$blob->{"start"} = TBDateStringGMT($row->{'start'});
$blob->{"end"} = TBDateStringGMT($row->{'end'});
$blob->{"notes"} = $row->{'notes'} || "";
$blob->{"approved"} = $row->{'approved'} || "";
......@@ -1040,21 +1070,29 @@ sub Reservations($)
sub ApproveReservation($)
{
my ($argref) = @_;
my $target;
my $hasperm = CheckPermission(1);
return $hasperm
if (GeniResponse::IsError($hasperm));
return GeniResponse->MalformedArgsResponse("Missing reservation ID")
if (! (exists($argref->{"idx"}) && $argref->{"idx"} ne ""));
my $idx = $argref->{"idx"};
return GeniResponse->MalformedArgsResponse("Illegal reservation ID")
if ($idx !~ /^\d+$/);
my $reservation = Reservation->Lookup($idx);
if (exists($argref->{'idx'})) {
return GeniResponse->MalformedArgsResponse("Illegal reservation idx")
if ($argref->{'idx'} !~ /^\d+$/);
$target = $argref->{'idx'};
}
elsif (exists($argref->{'uuid'})) {
return GeniResponse->MalformedArgsResponse("Illegal reservation uuid")
if (!ValidUUID($argref->{'uuid'}));
$target = $argref->{'uuid'};
}
else {
return GeniResponse->MalformedArgsResponse("Missing reservation ID");
}
my $reservation = Reservation->Lookup($target);
return GeniResponse->Create(GENIRESPONSE_SEARCHFAILED)
if (!defined($reservation));
my $idx = $reservation->idx();
GeniUtil::FlipToElabMan();
my $output = GeniUtil::ExecQuiet("$WAP $TB/sbin/reserve -a -m $idx");
......@@ -1064,8 +1102,20 @@ sub ApproveReservation($)
print STDERR "$output\n";
return GeniResponse->Create(GENIRESPONSE_REFUSED, undef, $output);
}
#
# Return current details of the reservation.
#
my $blob = {};
$blob->{"idx"} = $reservation->idx();
$blob->{"uuid"} = $reservation->uuid();
$blob->{"nodes"} = $reservation->nodes();
$blob->{"type"} = $reservation->type();
$blob->{"created"} = TBDateStringGMT($reservation->created());
$blob->{"start"} = TBDateStringGMT($reservation->start());
$blob->{"end"} = TBDateStringGMT($reservation->end());
GeniUtil::FlipToGeniUser();
return GeniResponse->Create(GENIRESPONSE_SUCCESS);
return GeniResponse->Create(GENIRESPONSE_SUCCESS, $blob);
}
#
......@@ -1076,6 +1126,7 @@ sub DeleteReservation($)
my ($argref) = @_;
my $args = "";
my $project;
my $target;
#
# The Portal decides the user has permission and then uses "admin" mode
......@@ -1085,13 +1136,19 @@ sub DeleteReservation($)
return $hasperm
if (GeniResponse::IsError($hasperm));
return GeniResponse->MalformedArgsResponse("Missing reservation ID")
if (! (exists($argref->{"idx"}) && $argref->{"idx"} ne ""));
my $idx = $argref->{"idx"};
return GeniResponse->MalformedArgsResponse("Illegal reservation ID")
if ($idx !~ /^\d+$/);
if (exists($argref->{'idx'})) {
return GeniResponse->MalformedArgsResponse("Illegal reservation idx")
if ($argref->{'idx'} !~ /^\d+$/);
$target = $argref->{'idx'};
}
elsif (exists($argref->{'uuid'})) {
return GeniResponse->MalformedArgsResponse("Illegal reservation uuid")
if (!ValidUUID($argref->{'uuid'}));
$target = $argref->{'uuid'};
}
else {
return GeniResponse->MalformedArgsResponse("Missing reservation ID");
}
return GeniResponse->MalformedArgsResponse("Missing project")
if (! (exists($argref->{"project"}) && $argref->{"project"} ne ""));
......@@ -1113,9 +1170,10 @@ sub DeleteReservation($)
return GeniResponse->MalformedArgsResponse("No such project")
if (!$project);
my $reservation = Reservation->Lookup($idx);
my $reservation = Reservation->Lookup($target);
return GeniResponse->Create(GENIRESPONSE_SEARCHFAILED)
if (!defined($reservation));
my $idx = $reservation->idx();
return GeniResponse->Create(GENIRESPONSE_FORBIDDEN)
if ($reservation->pid() ne $project->pid());
......
......@@ -116,8 +116,8 @@ $(function ()
*/
function DeleteReservation(row) {
// This is what we are deleting.
var idx = $(row).attr('data-idx');
var pid = $(row).attr('data-pid');
var uuid = $(row).attr('data-uuid');
var pid = $(row).attr('data-pid');
var cluster = $(row).attr('data-cluster');
var table = $(row).closest("table");
......@@ -138,8 +138,8 @@ $(function ()
sup.ShowModal('#waitwait-modal');
var xmlthing = sup.CallServerMethod(null, "reserve",
"Delete",
{"idx" : idx,
"pid" : pid,
{"uuid" : uuid,
"pid" : pid,
"cluster" : cluster});
xmlthing.done(callback);
});
......@@ -157,8 +157,8 @@ $(function ()
*/
function DenyReservation(row) {
// This is what we are deleting.
var idx = $(row).attr('data-idx');
var pid = $(row).attr('data-pid');
var uuid = $(row).attr('data-uuid');
var pid = $(row).attr('data-pid');
var cluster = $(row).attr('data-cluster');
var table = $(row).closest("table");
......@@ -180,8 +180,8 @@ $(function ()
sup.ShowModal('#waitwait-modal');
var xmlthing = sup.CallServerMethod(null, "reserve",
"Delete",
{"idx" : idx,
"pid" : pid,
{"uuid" : uuid,
"pid" : pid,
"cluster" : cluster,
"reason" : reason});
xmlthing.done(callback);
......
......@@ -299,6 +299,7 @@ $(function ()
function ValidateReservation()
{
var callback = function(json) {
console.info(json);
// Three indicates success but needs admin approval.
if (json.code) {
if (json.code != 2) {
......@@ -314,7 +315,7 @@ $(function ()
ToggleSubmit(true, "submit");
// Make sure we still warn about an unsaved form.
aptforms.MarkFormUnsaved();
if (json.value == 3) {
if (json.value.approved == 0) {
$('#confirm-reservation .needs-approval')
.removeClass("hidden");
}
......@@ -336,22 +337,13 @@ $(function ()
function Reserve()
{
var reserve_callback = function(json) {
console.info(json);
if (json.code) {
sup.SpitOops("oops", json.value);
return;
}
/*