Commit 2cef4a54 authored by Leigh Stoller's avatar Leigh Stoller

The rest of the reservation cancellation support. New checkbox on the

warning modal to schedule cancel in 24 hours. Also a new control to
cancel cancellation.
parent 48bcc554
......@@ -98,9 +98,12 @@ sub DoDelete();
sub DoApprove();
sub DoSystemInfo();
sub DoPrediction();
sub DoCancel();
sub readfile($);
sub AddAnnouncement($$$$$$$);
sub DeleteAnnouncement($);
sub RetireAnnouncement($);
sub RestoreAnnouncement($);
#
# Parse command arguments. Once we return from getopts, all that should be
......@@ -174,6 +177,9 @@ elsif ($action eq "systeminfo") {
elsif ($action eq "prediction") {
DoPrediction();
}
elsif ($action eq "cancel") {
DoCancel();
}
else {
usage();
}
......@@ -677,6 +683,93 @@ sub DoApprove()
exit(0);
}
#
# Schedule (or clear) reservation cancellation
#
sub DoCancel()
{
my $optlist = "p:ce:N:";
my $clear = 0;
my $when;
my $portal;
my $reason;
if (!$this_user->IsAdmin()) {
fatal("No permission to schedule reservation cancellation")
}
my %options = ();
if (! getopts($optlist, \%options)) {
usage();
}
if (defined($options{"N"})) {
$reason = readfile($options{"N"});
chomp($reason);
}
if (defined($options{"p"})) {
$portal = $options{"p"}
}
if (defined($options{"c"})) {
$clear = 1;
}
elsif (defined($options{"e"})) {
$when = $options{"e"};
if ($when !~ /^\d+$/) {
$when = str2time($when);
if (!defined($when)) {
fatal("Could not parse -e option.");
}
}
}
else {
usage();
}
usage()
if (@ARGV != 1 || !defined($portal));
my $uuid = shift(@ARGV);
# Check this since Reservation->Lookup() does not validate.
fatal("Invalid uuid")
if (!ValidUUID($uuid));
my $brand = Brand->Create($portal);
if (!defined($brand)) {
fatal("Bad branding");
}
my $args = {"uuid" => $uuid};
if ($clear) {
$args->{"clear"} = 1;
}
else {
$args->{"when"} = $when;
}
$args->{'reason'} = $reason if (defined($reason));
# PortalRPC will use the root context in this case, which is
# essentially saying the caller is an admin.
my $response =
APT_Geni::PortalRPC($authority, undef, "CancelReservation", $args);
if (GeniResponse::IsError($response)) {
#
# All errors are fatal.
#
fatal($response->output());
}
if (defined($webtask)) {
$webtask->Exited(0);
}
else {
print Dumper($response);
}
if ($clear) {
RestoreAnnouncement($uuid);
}
else {
RetireAnnouncement($uuid);
}
exit(0);
}
#
# Get the reservation system info.
#
......@@ -837,7 +930,7 @@ sub DoPrediction()
push(@projlist, $project->urn());
}
}
else {
elsif (!$this_user->IsAdmin()) {
my @plist;
if ($this_user->ProjectMembershipList(\@plist)) {
fatal("Could not get project membership list");
......@@ -1063,5 +1156,17 @@ sub DeleteAnnouncement($)
system("$ANNOUNCE -R $uuid");
}
sub RetireAnnouncement($)
{
my ($uuid) = @_;
system("$ANNOUNCE -r $uuid");
}
sub RestoreAnnouncement($)
{
my ($uuid) = @_;
system("$ANNOUNCE -w $uuid");
}
......@@ -990,7 +990,8 @@ sub Reservations($)
$query_result =
DBQueryWarn("select *,UNIX_TIMESTAMP(start) as start, ".
" UNIX_TIMESTAMP(end) as end, ".
" UNIX_TIMESTAMP(created) as created ".
" UNIX_TIMESTAMP(created) as created, ".
" UNIX_TIMESTAMP(cancel) as cancel ".
" from future_reservations ".
(defined($target) ? "where $target " : "") .
"order BY start");
......@@ -1007,7 +1008,8 @@ sub Reservations($)
$query_result =
DBQueryWarn("select *,UNIX_TIMESTAMP(start) as start, ".
" UNIX_TIMESTAMP(end) as end, ".
" UNIX_TIMESTAMP(created) as created ".
" UNIX_TIMESTAMP(created) as created, ".
" UNIX_TIMESTAMP(cancel) as cancel ".
" from future_reservations ".
"where pid='$pid' ".
(defined($target) ? "and $target" : ""));
......@@ -1017,7 +1019,8 @@ sub Reservations($)
$query_result =
DBQueryWarn("select *,UNIX_TIMESTAMP(start) as start, ".
" UNIX_TIMESTAMP(end) as end, ".
" UNIX_TIMESTAMP(created) as created ".
" UNIX_TIMESTAMP(created) as created, ".
" UNIX_TIMESTAMP(cancel) as cancel ".
" from future_reservations ".
"where uid='$uid'");
}
......@@ -1089,6 +1092,9 @@ sub Reservations($)
$blob->{"end"} = TBDateStringGMT($row->{'end'});
$blob->{"notes"} = $row->{'notes'} || "";
$blob->{"approved"} = $row->{'approved'} || "";
if (defined($row->{'cancel'})) {
$blob->{"cancel"} = TBDateStringGMT($row->{'cancel'});
}
$results{"$idx"} = $blob;
if (!exists($history{$projurn})) {
......@@ -1258,6 +1264,81 @@ sub DeleteReservation($)
return GeniResponse->Create(GENIRESPONSE_SUCCESS);
}
#
# Schedule (or clear) a reservation for destruction in the future.
#
sub CancelReservation($)
{
my ($argref) = @_;
my $args = "";
my $target;
#
# The Portal decides the user has permission and then uses "admin" mode
# to do the deletion.
#
my $hasperm = CheckPermission(1);
return $hasperm
if (GeniResponse::IsError($hasperm));
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");
}
if (exists($argref->{"clear"}) && $argref->{"clear"} == 1) {
$args = "-O ";
}
else {
return GeniResponse->MalformedArgsResponse("Missing termination date")
if (! (exists($argref->{"when"}) && $argref->{"when"} ne ""));
my $when = $argref->{"when"};
return GeniResponse->MalformedArgsResponse("Invalid termination date")
if (! ($when =~ /^\d+$/ || str2time($when)));
$args = "-E '$when' ";
}
my $reservation = Reservation->Lookup($target);
return GeniResponse->Create(GENIRESPONSE_SEARCHFAILED)
if (!defined($reservation));
my $idx = $reservation->idx();
# Write the reason to a tempfile to pass in. This will auto unlink.
my $fp;
if (exists($argref->{"reason"}) && $argref->{"reason"} ne "") {
my $reason = $argref->{"reason"};
if (!TBcheck_dbslot($reason, "default", "fulltext",
TBDB_CHECKDBSLOT_WARN|TBDB_CHECKDBSLOT_ERROR)) {
return GeniResponse->MalformedArgsResponse("Invalid reason")
}
$fp = File::Temp->new();
print $fp $reason;
$args .= " -D $fp";
chmod(0755, "$fp");
}
GeniUtil::FlipToElabMan();
my $output = GeniUtil::ExecQuiet("$WAP $TB/sbin/reserve -m $idx $args");
if ($?) {
GeniUtil::FlipToGeniUser();
print STDERR "$output\n";
return GeniResponse->Create(GENIRESPONSE_REFUSED, undef, $output);
}
GeniUtil::FlipToGeniUser();
return GeniResponse->Create(GENIRESPONSE_SUCCESS);
}
#
# Reservation info
#
......
......@@ -76,6 +76,22 @@ $(function ()
$('#main-body').prepend(html);
// Show the proper status now, we might change it later.
_.each(reservations, function(value, uuid) {
var id = '#' + name +
' tr[data-uuid="' + uuid + '"] .status-column';
if (value.cancel) {
$(id + " .status-canceled").removeClass("hidden");
}
else if (value.approved) {
$(id + " .status-approved").removeClass("hidden");
}
else {
$(id + " .status-pending").removeClass("hidden");
}
});
// Format dates with moment before display.
$('#' + name + ' .format-date').each(function() {
var date = $.trim($(this).html());
......@@ -109,6 +125,11 @@ $(function ()
ReservationInfoOrWarning("warn", $(this).closest('tr'));
return false;
});
// Bind a cancel cancellation handler.
$('#' + name + ' .cancel-cancel-button').click(function() {
CancelCancellation($(this).closest('tr'));
return false;
});
}
if (_.has(json.value, "history")) {
var history = json.value.history;
......@@ -281,7 +302,8 @@ $(function ()
var warning = (which == "warn" ? 1 : 0);
var modal = (warning ? "#warn-modal" : "#info-modal");
var method = (warning ? "WarnUser" : "RequestInfo");
var cancel = 0;
var callback = function (json) {
sup.HideModal('#waitwait-modal');
console.log("info/warn", json);
......@@ -289,6 +311,13 @@ $(function ()
sup.SpitOops("oops", json.value);
return;
}
// Reset the status column.
if (cancel) {
$(row).find(".status-column .status-approved")
.addClass("hidden");
$(row).find(".status-column .status-canceled")
.removeClass("hidden");
}
};
// Bind the confirm button in the modal.
$(modal + ' .confirm-button').click(function () {
......@@ -297,14 +326,21 @@ $(function ()
$(modal + ' .nomessage-error').removeClass("hidden");
return;
}
if (warning && $('#schedule-cancellation').is(":checked")) {
cancel = 1;
}
var args = {"uuid" : uuid,
"pid" : pid,
"uid_idx" : uid_idx,
"cluster" : cluster,
"cancel" : cancel,
"message" : message};
console.info("warninfo", args);
sup.HideModal(modal, function () {
sup.ShowModal('#waitwait-modal');
var xmlthing = sup.CallServerMethod(null, "reserve", method,
{"uuid" : uuid,
"pid" : pid,
"uid_idx" : uid_idx,
"cluster" : cluster,
"message" : message});
var xmlthing = sup.CallServerMethod(null, "reserve",
method, args);
xmlthing.done(callback);
});
});
......@@ -320,6 +356,49 @@ $(function ()
}
sup.ShowModal(modal);
}
function CancelCancellation(row) {
// This is what we are working on.
var uuid = $(row).attr('data-uuid');
var pid = $(row).attr('data-pid');
var cluster = $(row).attr('data-cluster');
var table = $(row).closest("table");
// Callback for the request.
var callback = function (json) {
sup.HideModal('#waitwait-modal');
if (json.code) {
console.log("cancel cancel", json);
sup.SpitOops("oops", json.value);
return;
}
// Reset the status column.
$(row).find(".status-column .status-canceled")
.addClass("hidden");
$(row).find(".status-column .status-approved")
.removeClass("hidden");
};
// Bind the confirm button in the modal.
$('#confirm-cancel-cancel-button').click(function () {
sup.HideModal('#cancel-cancel-modal', function () {
sup.ShowModal('#waitwait-modal');
var xmlthing = sup.CallServerMethod(null, "reserve",
"Cancel",
{"uuid" : uuid,
"clear" : 1,
"pid" : pid,
"cluster" : cluster});
xmlthing.done(callback);
});
});
// Handler so we know the user closed the modal. We need to
// clear the confirm button handler.
$('#cancel-cancel-modal').on('hidden.bs.modal', function (e) {
$('#confirm-cancel-cancel-button').unbind("click");
$('#cancel-cancel-modal').off('hidden.bs.modal');
})
sup.ShowModal("#cancel-cancel-modal");
}
// Helper.
function decodejson(id) {
......
......@@ -343,6 +343,7 @@ function Do_InfoOrWarn($warning = false)
global $this_user;
global $ajax_args;
global $TB_PROJECT_CREATEEXPT, $suexec_output, $APTBASE, $APTMAIL;
$cancel = 0;
if (!isset($ajax_args["uuid"])) {
SPITAJAX_ERROR(-1, "Missing uuid");
......@@ -386,11 +387,15 @@ function Do_InfoOrWarn($warning = false)
SPITAJAX_ERROR(-1, "No such user");
return;
}
if (!warning &&
if (!$warning &&
(!isset($ajax_args["message"]) || $ajax_args["message"] == "")) {
SPITAJAX_ERROR(-1, "Missing message");
return;
}
if ($warning &&
isset($ajax_args["cancel"]) && $ajax_args["cancel"] == 1) {
$cancel = 1;
}
if (!ISADMIN()) {
SPITAJAX_ERROR(-1, "Only administrators can ask for info or warn");
return;
......@@ -402,9 +407,14 @@ function Do_InfoOrWarn($warning = false)
$subject = "Unused Reservation Warning";
$message = "Your reservation in project $pid is currently either\n".
"underused or unused. Tying up resources in this manner means\n".
"other users may not be able to get their work done. If your\n".
"reservation is still underused in 24 hours, the reservation\n".
"will be deleted.\n\n";
"other users may not be able to get their work done.\n".
($cancel ?
"Your reservation is now scheduled to be canceled in 24 hours,\n".
"please contact the support staff if you would like to keep\n".
"your reservation.\n"
:
"If your reservation is still underused in 24 hours, it\n".
"will be canceled.\n") . "\n";
if (isset($ajax_args["message"]) && $ajax_args["message"] != "") {
$message .= $ajax_args["message"] . "\n";
......@@ -420,6 +430,11 @@ function Do_InfoOrWarn($warning = false)
"From: $APTMAIL\n".
"CC: $APTMAIL");
if ($cancel) {
if (Do_CancelAux($uuid, $aggregate, 0, time() + (3600 * 24))) {
return;
}
}
sleep(1);
SPITAJAX_RESPONSE(1);
}
......@@ -482,6 +497,76 @@ function Do_Approve()
SPITAJAX_RESPONSE("list-reservations.php");
}
#
# Cancel (or clear).
#
function Do_Cancel()
{
global $this_user;
global $ajax_args;
global $TB_PROJECT_CREATEEXPT, $suexec_output, $PORTAL_GENESIS;
if (!ISADMIN()) {
SPITAJAX_ERROR(-1, "Only administrators can cancel reservations");
return;
}
if (!isset($ajax_args["uuid"])) {
SPITAJAX_ERROR(-1, "Missing uuid");
return -1;
}
$uuid = $ajax_args["uuid"];
if (!IsValidUUID($uuid)) {
SPITAJAX_ERROR(-1, "Invalid uuid");
return;
}
if (!isset($ajax_args["clear"]) && $ajax_args["clear"] == 1) {
SPITAJAX_ERROR(-1, "Only clear supported.");
return;
}
if (!isset($ajax_args["cluster"])) {
SPITAJAX_ERROR(-1, "Missing cluster");
return;
}
if (!preg_match("/^[-\w]+$/", $ajax_args["cluster"])) {
SPITAJAX_ERROR(-1, "Invalid cluster name");
return;
}
$aggregate = Aggregate::LookupByNickname($ajax_args["cluster"]);
if (!$aggregate) {
SPITAJAX_ERROR(-1, "No such cluster");
return;
}
if (Do_CancelAux($uuid, $aggregate, 1, null)) {
return;
}
SPITAJAX_RESPONSE(0);
}
function Do_CancelAux($uuid, $aggregate, $clear, $when)
{
global $this_user;
global $TB_PROJECT_CREATEEXPT, $suexec_output, $PORTAL_GENESIS;
$this_uid = $this_user->uid();
$urn = $aggregate->urn();
if ($clear) {
$opts = "-c";
}
else {
$opts = "-e '$when'";
}
$retval = SUEXEC($this_uid, "nobody",
"webmanage_reservations -a '$urn' ".
" cancel -p $PORTAL_GENESIS $opts $uuid",
SUEXEC_ACTION_CONTINUE);
if ($retval) {
SPITAJAX_ERROR(-1, $suexec_output);
return;
}
}
#
# List reservations at a cluster (for a user).
#
......@@ -549,6 +634,12 @@ function Do_ListReservations()
else {
$blob["approved"] = null;
}
if (isset($details['cancel']) && $details['cancel'] != "") {
$blob["cancel"] = $details["cancel"];
}
else {
$blob["cancel"] = null;
}
$blob["project"] = $details['project'];
$blob["count"] = $details['nodes'];
$blob["type"] = $details['type'];
......
......@@ -360,6 +360,8 @@ $routing = array("myprofiles" =>
"Do_WarnUser",
"Delete" =>
"Do_Delete",
"Cancel" =>
"Do_Cancel",
"RequestInfo" =>
"Do_RequestInfo",
"ReservationInfo" =>
......
......@@ -174,11 +174,24 @@
<% } %>
</td>
<td class="format-date"><%- value.end %></td>
<% if (value.approved) { %>
<td>Approved</td>
<% } else {%>
<td><span class="text-danger">Pending</span></td>
<% } %>
<td class="status-column">
<span class="status-approved hidden">Approved</span>
<span class="status-pending hidden text-danger">Pending</span>
<span class="status-canceled hidden">
<span class="text-danger">Canceled</span>
<% if (isadmin) { %>
<a href="#" class="cancel-cancel-button">
<span class='glyphicon glyphicon-thumbs-up'
style='color: green; margin-left: 5px;
margin-bottom: 4px;'
data-toggle='tooltip'
data-container="body"
data-trigger="hover"
title='Cancel Cancellation'></span>
</a>
<% } %>
</span>
</td>
</tr>
<% }); %>
</tbody>
......@@ -238,8 +251,28 @@
<textarea class='form-control user-message'
rows=5></textarea>
</div>
<br>
<button class='btn btn-danger btn-sm confirm-button'>Confirm</button>
<input type=checkbox id='schedule-cancellation' value=yes>
Cancel reservation in 24 hours?
<div style="margin-top:10px;">
<button class='btn btn-danger btn-sm confirm-button'>Confirm</button>
</div>
</center>
</div>
</div>
</div>
</div>
<div id='cancel-cancel-modal' class='modal fade'>
<div class='modal-dialog'>
<div class='modal-content'>
<div class='modal-body'>
<button type='button' class='close' data-dismiss='modal'
aria-hidden='true'>&times;</button>
<center><h4>Confirm to Cancel Cancellation</h4>
<div style="margin-top:10px;">
<button id="confirm-cancel-cancel-button"
class='btn btn-danger btn-sm confirm-button'>
Confirm</button>
</div>
</center>
</div>
</div>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment