Commit 9fa5002c authored by Leigh Stoller's avatar Leigh 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'
data-content='Terminate an experiment with cause
and optionally freeze the user account.'>
Destroy</button>
<% } %>
</div>
<div class='pull-right'>
<% if (registered && !isfadmin) { %>
<span>
......@@ -1408,5 +1420,6 @@ class='fixedsize-panel with-3d-shadow with-transitions'>
<div id='oneonly_div'></div>
<div id='approval_div'></div>
<div id='linktest_div'></div>
<div id='destroy_div'></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