Commit 9fa5002c authored by Leigh B Stoller's avatar Leigh B Stoller

Add new UI to the status page for admins, to terminate an experiment

with cause and optionally freeze the user. "Cause" means you can paste
in a block of text that is emailed to the user.
parent b3da5798
......@@ -75,9 +75,13 @@ my $geniuser;
#
# Configure variables
#
my $TB = "@prefix@";
my $TBOPS = "@TBOPSEMAIL@";
my $QUICKVM = "$TB/sbin/protogeni/quickvm";
my $TB = "@prefix@";
my $TBOPS = "@TBOPSEMAIL@";
my $PROTOUSER = "elabman";
my $SUDO = "/usr/local/bin/sudo";
my $MANAGEINSTANCE = "$TB/bin/manage_instance";
my $WAP = "$TB/sbin/wap";
my $TBACCT = "$TB/sbin/tbacct";
#
# Untaint the path
......@@ -1237,16 +1241,19 @@ sub DoTerminate()
my $errcode;
my $exitcode = 1;
my $logfile;
my $expired = $RECORDHISTORY_TERMINATED;
my $takelock = 0;
my $expired = $RECORDHISTORY_TERMINATED;
if (@ARGV) {
my $arg = shift(@ARGV);
if ($arg eq "-e") {
$expired = $RECORDHISTORY_EXPIRED;
}
else {
usage();
}
my $optlist = "eL";
my %options = ();
if (! getopts($optlist, \%options)) {
usage();
}
if (defined($options{"e"})) {
$expired = $RECORDHISTORY_EXPIRED;
}
if (defined($options{"L"})) {
$takelock = 1;
}
my $slice = $instance->GetGeniSlice();
......@@ -1261,7 +1268,12 @@ sub DoTerminate()
# Lock the slice in case it is doing something else, like taking
# a disk image.
#
if ($slice->Lock()) {
# When told to take the lock, we take it go.
#
if ($takelock) {
$slice->TakeLock();
}
elsif ($slice->Lock()) {
#
# A special case is if the slice is provisioning. This means the
# user is giving up on it, and we want to tell the aggregate to
......@@ -1410,9 +1422,9 @@ sub DoTerminate()
}
#
# Destroy. Do not use this!
# Delete Do not use this!
#
sub DoDestroy()
sub DoDelete()
{
my $expired = $RECORDHISTORY_TERMINATED;
......@@ -4304,6 +4316,184 @@ sub DoSchedTerminate()
exit($exitcode);
}
#
# Terminate with cause and optionally freeze user. Send email.
#
sub DoDestroy()
{
my $errcode = 1;
my $exitcode = 1;
my $freeze = 0;
my $errmsg;
my $reason;
my $logfile;
my $brand = $instance->Brand();
my $creator = $instance->GetGeniUser();
my $slice = $instance->GetGeniSlice();
my $name = $instance->name();
my $pid = $instance->pid();
my $project = $instance->GetProject();
my $optlist = "f:F";
my %options = ();
if (! getopts($optlist, \%options)) {
usage();
}
if (defined($options{"F"})) {
$freeze = 1;
}
if (defined($options{"f"})) {
my $filename = $options{"f"};
if (! -e $filename) {
fatal("$filename does not exist");
}
open(MSG, $filename) or
fatal("Could not open $filename");
$reason = "";
while (<MSG>) {
$reason .= $_;
}
close(MSG);
}
if (!$this_user->IsAdmin()) {
fatal("Only admins can destroy experiments");
}
#
# Lock the slice in case it is doing something else, like taking
# a disk image.
#
if ($slice->Lock()) {
$errcode = GENIRESPONSE_BUSY;
$errmsg ="Experiment is busy, cannot lock it. Try again later.";
if (defined($webtask)) {
$webtask->output($errmsg);
$webtask->Exited($errcode);
}
print STDERR "$errmsg\n";
exit(1);
}
if (defined($reason) &&
!TBcheck_dbslot($reason, "default", "fulltext",
TBDB_CHECKDBSLOT_WARN|TBDB_CHECKDBSLOT_ERROR)) {
$errmsg = "Illegal characters in your reason";
$errcode = 1;
goto bad;
}
if ($instance->admin_lockdown()) {
$errmsg = "Must clear the admin lockdown first.";
$errcode = 1;
goto bad;
}
# No free time.
$instance->Update({"extension_disabled" => 1});
# Expiration is now.
$slice->SetExpiration(time());
# Now we can clear this.
if ($instance->user_lockdown()) {
if (DoLockdownInternal("clear", "user", 0, \$errmsg)) {
$errcode = 1;
goto bad;
}
}
# Hurry up the terminate instead of waiting for the daemon to see it.
if (!$debug) {
$logfile = TBMakeLogname("destroy");
if (my $childpid = TBBackGround($logfile)) {
my $status = 0;
#
# Wait a couple of seconds to see if there is going to be an
# immediate error. Then return and let it continue to run. This
# allows the web server to see quick errors. Later errors will
# have to be emailed.
#
sleep(5);
my $foo = waitpid($childpid, &WNOHANG);
if ($foo) {
$status = $? >> 8;
}
exit($status);
}
}
# We pass the lock through.
system("$MANAGEINSTANCE " . (defined($webtask) ? "-t $webtask_id " : "").
" -d -- terminate $uuid -L");
#
# We wait for this to finish since if its the local cluster, we cannot
# freeze before termination is complete cause of PROTOGENI_LOCALUSER.
# If there is an error terminating (other then busy), we are kinda
# screwed.
#
if ($?) {
if ($debug) {
exit ($? >> 8);
}
if (defined($webtask)) {
$webtask->Refresh();
print STDERR $webtask->output() . "\n";
$exitcode = $webtask->exitcode();
}
else {
$exitcode = $? >> 8;
}
my $slice_uuid = $slice->uuid();
my $weburl = $instance->webURL();
SENDMAIL($TBOPS,
"Unable to terminate instance with cause!",
"Pid: $pid\n".
"Name: $name\n".
"Slice: $slice_uuid\n".
"URL: $weburl\n".
"Reason:\n\n" . (defined($reason) ? $reason : "") . "\n",
$TBOPS, undef, $logfile);
unlink($logfile) if (defined($logfile));
exit($exitcode);
}
unlink($logfile) if (defined($logfile));
my $message = "Your experiment, $pid/$name, has been terminated!\n";
if ($freeze) {
$message .= "Your account has been frozen until this is resolved.\n";
}
if (defined($reason)) {
$message .= "Reason:\n\n" . $reason . "\n";
}
else {
$message .= "You will be contacted shortly with an explaination.\n";
}
$brand->SendEmail($creator->email(),
"Your experiment has been terminated with cause!",
$message,
$brand->OpsEmailAddress(),
"CC: " . $project->GetLeader()->email(),
"BCC: " . $brand->OpsEmailAddress());
# This will send email if it fails.
if ($freeze) {
my $creator_uid = $instance->creator();
system("$TBACCT -u freeze $creator_uid");
if ($?) {
exit($? >> 8);
}
}
exit(0);
bad:
print STDERR $errmsg . "\n";
if (defined($webtask)) {
$webtask->output($errmsg);
$webtask->Exited($errcode);
}
done:
exit($exitcode);
}
#
# Apply extension policies.
#
......
......@@ -1520,12 +1520,22 @@ sub GetAddressPools($)
my $count = GetText("count", $pool);
my $type = GetText("type", $pool);
my $cmurn = GetText("component_manager_id", $pool);
my $list = [];
my @ips = FindNodesNS("n:ipv4", $pool,
$EMULAB_NS)->get_nodelist();
foreach my $ipref (@ips) {
my $ip = GetText("address", $ipref);
my $mask = GetText("mask", $ipref);
push(@{$list}, {"ipv4" => $ip, "mask" => $mask});
}
push(@{ $result },
{
"client_id" => $client_id,
"count" => $count,
"type" => $type,
"cmurn" => $cmurn
"cmurn" => $cmurn,
"list" => $list,
});
}
return $result;
......
......@@ -22,6 +22,7 @@
# }}}
#
chdir("..");
include_once("webtask.php");
chdir("apt");
include_once("profile_defs.php");
include_once("instance_defs.php");
......@@ -139,8 +140,15 @@ function Do_ExperimentList()
else {
$cluster = $urn_mapping[$row["aggregate_urn"]];
}
$blob["name"] = "<a href='adminextend.php?uuid=$uuid'>$name</a>";
$blob["project"] = "<a href='show-project.php?project=$pid'>$pid</a>";
$namefrag = "<a href='adminextend.php?uuid=$uuid'>$name</a>
<a href='status.php?uuid=$uuid' target=_blank>
<span class='pull-right glyphicon glyphicon-eye-open'>
</span></a>";
$pidfrag = "<a href='show-project.php?project=$pid'>$pid</a>";
$blob["uuid"] = $uuid;
$blob["name"] = $namefrag;
$blob["project"] = $pidfrag;
$blob["cluster"] = $cluster;
$blob["pcount"] = $pcount;
$blob["phours"] = $phours;
......@@ -276,6 +284,45 @@ function Do_ExperimentErrors()
SPITAJAX_RESPONSE($results);
}
#
# Search for an IP.
#
function Do_SearchIP()
{
global $this_user, $urn_mapping;
global $ajax_args;
$this_uid = $this_user->uid();
if (CheckPageArgs()) {
return;
}
$ip = $ajax_args["ip"];
if (! preg_match('/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/', $ip)) {
SPITAJAX_ERROR(-1, "Bad IP address");
return;
}
$webtask = WebTask::CreateAnonymous();
$retval = SUEXEC($this_uid, "nobody",
"websearchip -t " . $webtask->task_id() . " $ip",
SUEXEC_ACTION_IGNORE);
$webtask->Refresh();
if ($retval != 0) {
if (!$webtask->exited() || $retval < 0) {
SUEXECERROR(SUEXEC_ACTION_CONTINUE);
SPITAJAX_ERROR(-1, "Internal error");
}
else {
SPITAJAX_ERROR($webtask->exitcode(), $webtask->output());
}
$webtask->Delete();
return;
}
$uuid = $webtask->TaskValue("instance");
$webtask->Delete();
SPITAJAX_RESPONSE($uuid);
}
# Local Variables:
# mode:php
# End:
......
......@@ -2,7 +2,7 @@ $(function ()
{
'use strict';
var templates = APT_OPTIONS.fetchTemplateList(['status', 'waitwait-modal', 'oops-modal', 'register-modal', 'terminate-modal', 'oneonly-modal', 'approval-modal', 'linktest-modal', 'linktest-md']);
var templates = APT_OPTIONS.fetchTemplateList(['status', 'waitwait-modal', 'oops-modal', 'register-modal', 'terminate-modal', 'oneonly-modal', 'approval-modal', 'linktest-modal', 'linktest-md', "destroy-experiment"]);
var statusString = templates['status'];
var waitwaitString = templates['waitwait-modal'];
......@@ -12,6 +12,7 @@ $(function ()
var oneonlyString = templates['oneonly-modal'];
var approvalString = templates['approval-modal'];
var linktestString = templates['linktest-modal'];
var destroyString = templates['destroy-experiment'];
var expinfo = null;
var nodecount = 0;
......@@ -130,6 +131,7 @@ $(function ()
$('#oneonly_div').html(oneonlyString);
$('#approval_div').html(approvalString);
$('#linktest_div').html(linktestString);
$('#destroy_div').html(destroyString);
// Not allowed to copy repobased profiles.
if (expinfo.repourl) {
......@@ -262,6 +264,12 @@ $(function ()
lockdown_override});
xmlthing.done(callback);
});
// Destroy an experiment.
$('#destroy-experiment-button').click(function (event) {
event.preventDefault();
DestroyExperiment();
});
// Handler for select/deselect all rows in the list view.
$('#select-all').change(function () {
if ($(this).prop("checked")) {
......@@ -618,6 +626,7 @@ $(function ()
var reloadtopo;
var extend;
var snapshot;
var destroy;
switch (status)
{
......@@ -628,21 +637,23 @@ $(function ()
case 'terminated':
case 'unknown':
terminate = refresh = reloadtopo = extend = snapshot = 0;
destroy = 0;
break;
case 'provisioned':
case 'deferred':
refresh = reloadtopo = extend = snapshot = 0;
refresh = reloadtopo = extend = snapshot = destroy = 0;
terminate = 1;
break;
case 'ready':
terminate = refresh = reloadtopo = extend = snapshot = 1;
destroy = 1;
break;
case 'failed':
case 'imaging-failed':
refresh = reloadtopo = terminate = 1;
refresh = reloadtopo = terminate = destroy = 1;
extend = snapshot = 0;
break;
}
......@@ -656,6 +667,7 @@ $(function ()
ButtonState('reloadtopo', reloadtopo);
ButtonState('extend', extend);
ButtonState('snapshot', snapshot);
ButtonState('destroy', destroy);
ToggleLinktestButtons(status);
}
function EnableButton(button)
......@@ -675,6 +687,8 @@ $(function ()
enable = 0;
}
}
else if (button == "destroy")
button = "#destroy-experiment-button";
else if (button == "extend")
button = "#extend_button";
else if (button == "refresh")
......@@ -3435,6 +3449,50 @@ $(function ()
});
}
/*
* Terminate with cause and optionally freeze user.
*/
function DestroyExperiment()
{
// Handler for the Snapshot confirm button.
$('#destroy-experiment-confirm')
.bind("click.destroy", function (event) {
event.preventDefault();
var reason = $('#destroy-experiment-reason').val();
var freeze = $('#freeze-user-checkbox').is(':checked');
var args = {"uuid" : uuid};
if (reason != "") {
args["reason"] = reason;
}
if (freeze) {
args["freeze"] = true;
}
sup.HideModal("#destroy-experiment-modal", function () {
sup.ShowWaitWait();
sup.CallServerMethod(null, "status", "Destroy", args,
function(json) {
console.info("destroy", json);
if (json.code) {
sup.HideWaitWait(function () {
sup.SpitOops("oops",
"Could not terminate experiment: " +
json.value);
});
return;
}
sup.HideWaitWait();
});
});
});
// Handler for hide modal to unbind the click handler.
$('#destroy-experiment-modal').on('hidden.bs.modal', function (event) {
$(this).unbind(event);
$('#destroy-experiment-confirm').unbind("click.destroy");
});
sup.ShowModal("#destroy-experiment-modal");
}
// Helper.
function decodejson(id) {
return JSON.parse(_.unescape($(id)[0].textContent));
......
......@@ -186,6 +186,8 @@ $routing = array("geni-login" =>
"Do_Lockout",
"Lockdown" =>
"Do_Lockdown",
"Destroy" =>
"Do_DestroyExperiment",
"Quarantine" =>
"Do_Quarantine",
"SaveAdminNotes" =>
......
......@@ -277,7 +277,7 @@ function Do_TerminateInstance()
# that gets returned, that is not also emailed by the script. So just
# use the ignore option.
$retval = SUEXEC("nobody", "nobody",
"webmanage_instance -t $webtask_id terminate $uuid",
"webmanage_instance -t $webtask_id -- terminate $uuid",
SUEXEC_ACTION_IGNORE);
if ($retval) {
$webtask->Refresh();
......@@ -1868,6 +1868,73 @@ function Do_GetHealthStatus()
SPITAJAX_RESPONSE($status);
}
function Do_DestroyExperiment()
{
global $instance, $creator, $this_user;
global $ajax_args;
$this_uid = $this_user->uid();
$options = "";
# Really, only admins can do this.
if (StatusSetupAjax(0)) {
goto bad;
}
$uuid = $instance->uuid();
$slice = GeniSlice::Lookup("sa", $instance->slice_uuid());
if (!slice) {
SPITAJAX_ERROR(1, "no slice for instance");
goto bad;
}
if (!ISADMIN()) {
SPITAJAX_ERROR(1, "You do not have permission to do this.");
goto bad;
}
if (isset($ajax_args["reason"]) && $ajax_args["reason"] != "") {
$reason = $ajax_args["reason"];
if (!TBvalid_fulltext($reason)) {
SPITAJAX_ERROR(1, "Illegal characters in message");
goto bad;
}
$filename = tempnam("/tmp", "reason");
$fp = fopen($filename, "w");
fwrite($fp, $reason);
fclose($fp);
chmod($filename, 0666);
$options = "-f $filename ";
}
# Freeze user after experiment terminated.
if (isset($ajax_args["freeze"]) && $ajax_args["freeze"]) {
$options .= "-F";
}
$webtask = WebTask::CreateAnonymous();
$retval = SUEXEC($this_uid, "nobody",
"webmanage_instance -t " . $webtask->task_id() . " -- ".
" destroy $uuid $options ",
SUEXEC_ACTION_IGNORE);
$webtask->Refresh();
if (isset($filename)) {
unlink($filename);
}
if ($retval != 0) {
if (!$webtask->exited() || $retval < 0) {
SUEXECERROR(SUEXEC_ACTION_CONTINUE);
SPITAJAX_ERROR(-1, "Internal error");
}
else {
# Need to pass exitcode through on this one.
SPITAJAX_ERROR($webtask->exitcode(), $webtask->output());
}
$webtask->Delete();
return;
}
$webtask->Delete();
SPITAJAX_RESPONSE("Success");
return;
bad:
sleep(1);
}
# Local Variables:
# mode:php
# End:
......
......@@ -315,7 +315,10 @@ if (isset($this_user)) {
echo "</script>\n";
}
AddTemplateList(array("status", "waitwait-modal", "oops-modal", "register-modal", "terminate-modal", "oneonly-modal", "approval-modal", "linktest-modal"));
AddTemplateList(array("status", "waitwait-modal", "oops-modal",
"register-modal", "terminate-modal", "oneonly-modal",
"approval-modal", "linktest-modal",
"destroy-experiment"));
AddTemplateKey("linktest-md", "template/linktest.md");
SPITFOOTER();
?>
<!-- This is the terminate modal -->
<div id='destroy-experiment-modal' class='modal fade'>
<div class='modal-dialog'>
<div class='modal-content'>
<div class="modal-header">
<button type='button' class='close' data-dismiss='modal'
aria-hidden='true'>&times;</button>
<span class="text-center">
<h4>Terminate experiment with cause</h4></span>
</div>
<div class='modal-body'>
<p>
Are you sure you want to terminate this experiment and
optionally freeze the experimentor? Please provide a
reason below.
</p>
<center>
<div>
<textarea id='destroy-experiment-reason'
class='form-control' rows=5></textarea>
</div>
<input type=checkbox id='freeze-user-checkbox' value=yes>
Freeze User?
<div style="margin-top: 10px;">
<button class='btn btn-primary'
style="margin-right: 10px;"
data-dismiss="modal">Cancel</button>
<button class='btn btn-danger'
id='destroy-experiment-confirm'
type='submit' name='destroy'>Terminate</button>
</div>
</center>
</div>
</div>
</div>
</div>
......@@ -197,6 +197,18 @@ pre {
target='_blank'
type='button'>Stitcher</a>
</div>
<% if (isadmin) { %>
<div class='pull-left'>
<button class='btn btn-xs btn-danger' disabled
style='margin-left: 10px;'
style='margin-right: 10px;'
id='destroy-experiment-button' type=button
data-toggle='popover'