Commit f71d7d95 authored by Leigh Stoller's avatar Leigh Stoller

Add extension limiting to manage_extensions and request extension paths.

The limit is the number of hours since the experiment is created, so a
limit of 10 days really just means that experiments can not live past 10
days. I think this makes more sense then anything else. There is an
associated flag with extension limiting that controls whether the user
can even request another extension after the limit. The normal case is
that the user cannot request any more extensions, but when set, the user
is granted no free time and goes through need admin approval path.

Some changes to the email, so that both the user and admin email days
how many days/hours were both requested and granted.

Also UI change; explicitly tell the user when extensions are disabled,
and also when no time is granted (so that the users is more clearly
aware).
parent c751377d
......@@ -398,7 +398,10 @@ sub Update($$)
my $uuid = $self->uuid();
my $query = "update apt_instances set ".
join(",", map("$_=" . DBQuoteSpecial($argref->{$_}), keys(%{$argref})));
join(",", map("$_=" .
(defined($argref->{$_}) ?
DBQuoteSpecial($argref->{$_}) : "NULL"),
keys(%{$argref})));
$query .= " where uuid='$uuid'";
......@@ -1195,79 +1198,113 @@ sub ApplyExtensionPolicies($)
my $pid_idx = $self->pid_idx();
my $gid_idx = $self->gid_idx();
my $uid_idx = $self->creator_idx();
my $current = $self->extension_disabled();
my $current_disable = $self->extension_disabled();
my $current_limit = $self->extension_limit();
my $policy;
my $disabled = 0;
my $limit = 0;
my $admin_after_limit = 0;
my $reason;
#
# Apply in order project, group, then user.
#
my $query_result =
DBQueryWarn("select disabled,reason from apt_extension_group_policies ".
DBQueryWarn("select disabled,reason,`limit`,admin_after_limit ".
" from apt_extension_group_policies ".
"where pid_idx='$pid_idx' and gid_idx=pid_idx");
return -1
if (!defined($query_result));
if ($query_result->numrows) {
($disabled,$reason) = $query_result->fetchrow_array();
if ($disabled && !defined($reason)) {
($disabled,$reason,
$limit,$admin_after_limit) = $query_result->fetchrow_array();
if (($disabled || $limit) && !defined($reason)) {
$reason = "project restriction";
}
$policy = "Project";
}
$query_result =
DBQueryWarn("select disabled,reason from apt_extension_group_policies ".
DBQueryWarn("select disabled,reason,`limit`,admin_after_limit ".
" from apt_extension_group_policies ".
"where pid_idx='$pid_idx' and gid_idx='$gid_idx'");
return -1
if (!defined($query_result));
if ($query_result->numrows) {
my ($d,$r) = $query_result->fetchrow_array();
if ($d) {
$disabled = 1;
$reason = (defined($r) ? $r : "group restriction");
}
else {
$disabled = 0;
$reason = undef;
my ($d,$r,$l,$admin_after_limit) = $query_result->fetchrow_array();
$disabled = $d;
$limit = $l;
$reason = $r;
$policy = "Group";
if (($disabled || $limit) && !defined($reason)) {
$reason = "group restriction";
}
$policy = "Group";
}
$query_result =
DBQueryWarn("select disabled,reason from apt_extension_user_policies ".
DBQueryWarn("select disabled,reason,`limit`,admin_after_limit ".
" from apt_extension_user_policies ".
"where uid_idx='$uid_idx'");
return -1
if (!defined($query_result));
if ($query_result->numrows) {
my ($d,$r) = $query_result->fetchrow_array();
if ($d) {
$disabled = 1;
$reason = (defined($r) ? $r : "user restriction");
my ($d,$r,$l,$admin_after_limit) = $query_result->fetchrow_array();
$disabled = $d;
$limit = $l;
$reason = $r;
$policy = "User";
if (($disabled || $limit) && !defined($reason)) {
$reason = "user restriction";
}
else {
$disabled = 0;
$reason = undef;
}
$policy = "User";
}
# Apply disabled flag
$self->Update({"extension_disabled" => $disabled}) == 0
or return -1;
# Apply flags
$self->Update({"extension_disabled" => $disabled,
"extension_limit" => $limit,
"extension_admin_after_limit" => $admin_after_limit})
== 0 or return -1;
# Set the reason only if disabled, clear otherwise.
if ($disabled && defined($reason)) {
$self->Update({"extension_disabled_reason" => $reason}) == 0
if ($disabled) {
$self->Update({"extension_disabled_reason" => $reason}) == 0
or return -1;
}
else {
$self->Update({"extension_disabled_reason" => undef}) == 0
or return -1;
}
# Ditto the limit.
if ($limit) {
$self->Update({"extension_limit_reason" => $reason}) == 0
or return -1;
}
else {
DBQueryWarn("update apt_instances set extension_disabled_reason=NULL ".
"where uuid='$uuid'")
$self->Update({"extension_limit_reason" => undef}) == 0
or return -1;
}
if ($disabled != $current) {
my $which = ($disabled ? "disabled" : "enabled");
#
# Basically, disable overrides limit when requesting an extension.
#
$current_limit = -1 if (!defined($current_limit));
$limit = -1 if (!defined($limit));
if ($disabled != $current_disable || $limit != $current_limit) {
my $message = "$policy policy applied to $pid/$name:\n";
if ($disabled != $current_disable) {
my $which = ($disabled ? "disabled" : "enabled");
$message .= "Extensions have been ${which}.\n";
}
if ($limit != $current_limit) {
my $which = ($limit ? "limited to $limit days" :
"set to unlimited");
$message .= "Extensions have been ${which}.\n";
if ($limit && $admin_after_limit) {
$message .=
"Additional extension requests will require ".
"admin approval.\n";
}
}
SENDMAIL($TBAUDIT,
"Portal experiment $uuid extensions $which",
"$policy policy has $which extensions for $pid/$name\n\n".
"Portal experiment $uuid extension policy changed",
$message . "\n" .
(defined($reason) ? "Reason:\n$reason\n\n" : "").
$self->adminURL() . "\n",
$TBOPS);
......@@ -1275,6 +1312,34 @@ sub ApplyExtensionPolicies($)
return 0;
}
#
# Convert hours to days and hours string.
#
sub HoursToEnglish($)
{
my ($hours) = @_;
my $string;
my $wdays = int($hours / 24);
my $whours = $hours % 24;
if ($wdays) {
$string = "$wdays day";
$string .= "s" if ($wdays > 1);
if ($whours) {
$string .= " ${whours} hour";
$string .= "s" if ($whours > 1);
}
}
elsif ($whours) {
$string = "$whours hour";
$string .= "s" if ($whours > 1);
}
else {
$string = "nothing";
}
return $string;
}
###################################################################
package APT_Instance::ExtensionInfo;
use emdb;
......
......@@ -38,14 +38,20 @@ sub usage()
" manage_extensions disable [-a] [-m reason]".
" -p pid | -g pid,gid | -u uid\n".
" manage_extensions enable [-a] -p pid | -g pid,gid | -u uid\n".
" manage_extensions limit [-a] [-m reason] -A ".
" -p pid | -g pid,gid | -u uid <days>\n".
" manage_extensions unlimit [-a] -p pid | -g pid,gid | -u uid\n".
" manage_extensions remove [-a] -p pid | -g pid,gid | -u uid\n".
" manage_extensions show [-p pid | -u uid]\n".
" manage_extensions apply [-p pid | -u uid]\n".
"Options:\n".
" -p - Apply to entire project.\n".
" -g - Apply to project group. \n".
" -u - Apply to user.\n".
" -m - Short explanation if desired.\n".
" -a - Apply changes to matching experiments.\n"
" -a - Apply changes to matching experiments.\n".
" -A - For 'limit', extensions require admin approval\n".
" instead of being outright denied.\n"
);
exit(-1);
}
......@@ -87,8 +93,9 @@ use APT_Instance;
# Protos
sub fatal($);
sub DoEnableDisable($);
sub DoAction($);
sub DoShow();
sub DoApply();
sub Apply($);
# Locals
......@@ -118,12 +125,16 @@ if (!$this_user->IsAdmin()) {
fatal("Only admins can do this");
}
if ($action eq "disable" || $action eq "enable" || $action eq "remove") {
DoEnableDisable($action);
if ($action eq "disable" || $action eq "enable" || $action eq "remove" ||
$action eq "limit" || $action eq "unlimit") {
DoAction($action);
}
elsif ($action eq "show") {
DoShow();
}
elsif ($action eq "apply") {
DoApply();
}
else {
usage();
}
......@@ -132,11 +143,11 @@ exit(0);
#
# Enable/Disable extensions.
#
sub DoEnableDisable($)
sub DoAction($)
{
my ($action) = @_;
my $optlist = "ap:g:u:m:";
my $optlist = "ap:g:u:m:A";
my $apply = 0;
my $target;
my $reason;
......@@ -197,7 +208,33 @@ sub DoEnableDisable($)
}
}
else {
my $disabled = ($action eq "disable" ? 1 : 0);
my $clause = "";
if ($action eq "enable" || $action eq "disable") {
my $disabled = ($action eq "disable" ? 1 : 0);
$clause = "disabled='$disabled'";
}
elsif ($action eq "limit" || $action eq "unlimit") {
# When setting a limit, clear the disable flag.
$clause = "disabled='0',`limit`=";
if ($action eq "unlimit") {
$clause .= "'0'";
}
else {
usage()
if (!@ARGV);
my $days = shift(@ARGV);
usage()
if ($days !~ /^\d+$/);
$clause .= "'" . $days * 24 . "'";
if (defined($options{"A"})) {
$clause .= ",admin_after_limit='1'";
}
else {
$clause .= ",admin_after_limit='0'";
}
}
}
my $reason_clause = "";
if (defined($reason)) {
$reason_clause = "reason=" . DBQuoteSpecial($reason) . ", ";
......@@ -207,7 +244,7 @@ sub DoEnableDisable($)
my $pid = $target->pid();
my $pid_idx = $target->pid_idx();
DBQueryFatal("replace into apt_extension_group_policies set ".
" disabled='$disabled', created=now(), ".
" $clause, created=now(), ".
" creator='$creator', creator_idx='$creator_idx', ".
" $reason_clause ".
" pid='$pid', gid='$pid', ".
......@@ -219,7 +256,7 @@ sub DoEnableDisable($)
my $gid = $target->gid();
my $gid_idx = $target->gid_idx();
DBQueryFatal("replace into apt_extension_group_policies set ".
" disabled='$disabled', created=now(), ".
" $clause, created=now(), ".
" creator='$creator', creator_idx='$creator_idx', ".
" $reason_clause ".
" pid='$pid', gid='$gid', ".
......@@ -229,7 +266,7 @@ sub DoEnableDisable($)
my $uid = $target->uid();
my $uid_idx = $target->uid_idx();
DBQueryFatal("replace into apt_extension_user_policies set ".
" disabled='$disabled', created=now(), ".
" $clause, created=now(), ".
" creator='$creator', creator_idx='$creator_idx', ".
" $reason_clause ".
" uid_idx='$uid_idx', uid='$uid'");
......@@ -334,6 +371,36 @@ sub DoShow()
exit(0);
}
#
# Apply policies to target.
#
sub DoApply()
{
my $optlist = "p:u:";
my $target;
my %options = ();
if (! getopts($optlist, \%options)) {
usage();
}
if (defined($options{"p"})) {
$target = Project->Lookup($options{"p"});
fatal("No such project")
if (!defined($target));
}
elsif (defined($options{"u"})) {
$target = User->Lookup($options{"u"});
fatal("No such user")
if (!defined($target));
}
else {
usage();
}
Apply($target);
return 0;
}
sub Apply($)
{
my ($target) = @_;
......@@ -356,7 +423,10 @@ sub Apply($)
my $instance = APT_Instance->Lookup($uuid);
next
if (!defined($instance));
my $current = $instance->extension_disabled();
my $current_disabled = $instance->extension_disabled();
my $current_limit = $instance->extension_limit();
# Perl is stupid.
$current_limit = -1 if (!defined($current_limit));
if ($instance->ApplyExtensionPolicies()) {
print STDERR "Could not apply extension policies to $instance\n";
......@@ -365,12 +435,19 @@ sub Apply($)
$instance->Refresh();
my $disabled = ($instance->extension_disabled() ?
"disabled" : "enabled");
my $which = ($current != $instance->extension_disabled() ?
"now" : "still");
my $pid = $instance->pid();
my $name = $instance->name();
my $limit = $instance->extension_limit();
$limit = -1 if (!defined($limit));
my $limited = ($limit > 0 ?
"limited to " . APT_Instance::HoursToEnglish($limit) :
"unlimited");
my $pid = $instance->pid();
my $name = $instance->name();
my $which1 = ($current_disabled != $instance->extension_disabled() ?
"now" : "still");
my $which2 = ($current_limit != $limit ? "now" : "still");
print "Extensions for $pid/$name are $which $disabled\n";
print "Extensions for $pid/$name are $which1 $disabled\n";
print "Extensions for $pid/$name are $which2 $limited\n";
}
}
......
......@@ -1491,7 +1491,7 @@ sub DoExtend()
# Need to return this to the web interface via the webtask.
return "Your request requires admininstrator approval".
($message ? " because $message" : "") . ". " .
($message ? " $message" : "") . ". " .
"You will receive email if/when your ".
"request is granted (or denied). Thanks!";
};
......@@ -1505,9 +1505,9 @@ sub DoExtend()
}
#
# Always grant if <= 24 hours.
# Always grant if < 24 hours, unless an extension limit.
#
if ($wanted <= 24) {
if ($wanted < 24 && !$instance->extension_limit()) {
$message = "Short extension granted for $wanted hours.";
$reason = $message if (!defined($reason));
$granted = $wanted;
......@@ -1520,7 +1520,7 @@ sub DoExtend()
$granted = $wanted;
}
elsif (0) {
$message = "Testing extension stuff";
$message = "because we are testing extension stuff";
$granted = 0;
$needapproval = 1;
}
......@@ -1529,7 +1529,9 @@ sub DoExtend()
my $cdiff = time() - $created_time;
if (! defined($reason)) {
fatal("You must supply a reason for this extension");
$errcode = -1;
$errmsg = "You must supply a reason for this extension";
goto bad;
}
#
......@@ -1540,6 +1542,57 @@ sub DoExtend()
$granted = 0;
}
#
# Is there an extension limit.
#
elsif ($instance->extension_limit()) {
my $diff = $expires_time - $created_time;
my $max = 3600 * $instance->extension_limit();
if ($diff >= $max) {
#
# Already out to the limit.
#
$granted = 0;
}
else {
#
# Give them up to as much as the extension limits them to.
# Note that this could turn out to be zero.
#
if ($diff + ($wanted * 3600) > $max) {
$granted = int(($max - $diff) / 3600);
$message = "because your extension is limited by local ".
"policy to " . APT_Instance::HoursToEnglish($granted);
}
else {
$granted = $wanted;
}
}
if ($granted == 0) {
if (defined($instance->extension_limit_reason())) {
$message = $instance->extension_limit_reason();
}
else {
$message = "because you have exceeded the maximum ".
"extension allowed by local policy";
}
#
# Mark as admin disabled now so they do not get to ask again,
# but not if admin_after_limit is set, then it goes through
# the need admin approval path.
#
if ($instance->extension_admin_after_limit()) {
$needapproval = 1;
}
else {
$instance->Update({"extension_disabled" => 1,
"extension_disabled_reason" =>$message});
}
}
# Avoid any change to granted below.
goto grant;
}
#
# After maxage, all extension requests require admin approval.
#
elsif ($cdiff > (3600 * 24 * $autoextend_maxage)) {
......@@ -1642,31 +1695,8 @@ sub DoExtend()
localtime($expires_time));
# Format hours into days/hours.
my $wdays = int($wanted / 24);
my $whours = $wanted % 24;
my $gdays = int($granted / 24);
my $ghours = $granted % 24;
if ($wdays) {
$wantstring = "$wdays days";
if ($whours) {
$wantstring .= " ${whours} hours";
}
}
else {
$wantstring = "$whours hours";
}
if ($gdays) {
$grantstring = "$gdays days";
if ($ghours) {
$grantstring .= " ${ghours} hours";
}
}
elsif ($ghours) {
$grantstring = "$ghours hours";
}
else {
$grantstring = "nothing";
}
$wantstring = APT_Instance::HoursToEnglish($wanted);
$grantstring = APT_Instance::HoursToEnglish($granted);
#
# New extension mechanism
......@@ -1730,13 +1760,31 @@ sub DoExtend()
$errcode = 2;
goto bad;
}
my $mailmessage = $message;
if ($this_user->IsAdmin()) {
$mailmessage .= "\n\n" . $reason if (defined($reason));
}
else {
my $pre = "A request to extend your experiment was made, you were\n".
"granted ";
if ($granted == 0) {
$pre .= "nothing";
if (defined($mailmessage)) {
$pre .= " $mailmessage";
}
$mailmessage = $pre . ". ";
}
else {
$mailmessage = $pre . $grantstring . ". ";
}
$mailmessage .= "\n\n" . "The request was for ${wantstring}.";
$mailmessage .= "\n\nYour reason was:\n${reason}"
if (defined($reason));
}
$instance->Brand()->SendEmail($creator->email(),
"Experiment Extension: $name",
($this_user->IsAdmin() ?
"$message\n\n" . (defined($reason) ? $reason : "") :
"A request to extend your experiment was made and ".
"granted.\n".
(defined($reason) ? "Your reason was:\n\n${reason}" : "")) .
"Experiment Extension: $name",
$mailmessage .
"\n\n".
"Your experiment was started on $created\n".
"Your experiment will now expire at $expires\n".
......@@ -1764,6 +1812,8 @@ sub DoExtend()
}
$instance->BumpExtensionCount($granted);
if (defined($webtask)) {
$webtask->granted($granted);
$webtask->message(defined($message) ? $message : "");
$webtask->Exited(0);
}
$slice->UnLock();
......
......@@ -186,6 +186,8 @@ CREATE TABLE `apt_extension_group_policies` (
`creator` varchar(8) default NULL,
`creator_idx` mediumint(8) unsigned default NULL,
`disabled` tinyint(1) NOT NULL default '0',
`limit` int(10) unsigned default NULL,
`admin_after_limit tinyint(1) NOT NULL default '0',
`created` datetime default NULL,
`reason` mediumtext,
PRIMARY KEY (`pid_idx`,`gid_idx`)
......@@ -202,6 +204,8 @@ CREATE TABLE `apt_extension_user_policies` (
`creator` varchar(8) default NULL,
`creator_idx` mediumint(8) unsigned default NULL,
`disabled` tinyint(1) NOT NULL default '0',
`limit` int(10) unsigned default NULL,
`admin_after_limit tinyint(1) NOT NULL default '0',
`created` datetime default NULL,
`reason` mediumtext,
PRIMARY KEY (`uid_idx`)
......@@ -407,6 +411,9 @@ CREATE TABLE `apt_instances` (
`extension_adminonly` tinyint(1) NOT NULL default '0',
`extension_disabled` tinyint(1) NOT NULL default '0',
`extension_disabled_reason` mediumtext,
`extension_limit` int(10) unsigned default NULL,
`extension_limit_reason` mediumtext,
`extension_admin_after_limit tinyint(1) NOT NULL default '0',
`extension_requested` tinyint(1) NOT NULL default '0',
`extension_denied` tinyint(1) NOT NULL default '0',
`extension_denied_reason` mediumtext,
......
use strict;
use libdb;
sub DoUpdate($$$)
{
my ($dbhandle, $dbname, $version) = @_;
if (!DBSlotExists("apt_instances", "extension_limit")) {
DBQueryFatal("alter table apt_instances add ".
" `extension_limit` int(10) unsigned default NULL ".
" after extension_disabled_reason");
}
if (!DBSlotExists("apt_instances", "extension_limit_reason")) {
DBQueryFatal("alter table apt_instances add ".
" `extension_limit_reason` mediumtext ".
" after extension_limit ");
}
if (!DBSlotExists("apt_instances", "extension_admin_after_limit")) {
DBQueryFatal("alter table apt_instances add ".
" `extension_admin_after_limit` ".
" tinyint(1) NOT NULL default '0' ".
" after extension_limit_reason ");
}
if (!DBSlotExists("apt_extension_group_policies", "limit")) {
DBQueryFatal("alter table apt_extension_group_policies add ".
" `limit` int(10) unsigned default NULL ".
" after disabled");
}
if (!DBSlotExists("apt_extension_group_policies", "admin_after_limit")) {
DBQueryFatal("alter table apt_extension_group_policies add ".
" `admin_after_limit` tinyint(1) NOT NULL default '0' ".
" after `limit` ");
}
if (!DBSlotExists("apt_extension_user_policies", "limit")) {
DBQueryFatal("alter table apt_extension_user_policies add ".
" `limit` int(10) unsigned default NULL ".
" after disabled");
}
if (!DBSlotExists("apt_extension_user_policies", "admin_after_limit")) {
DBQueryFatal("alter table apt_extension_user_policies add ".
" `admin_after_limit` tinyint(1) NOT NULL default '0' ".
" after `limit` ");
}
return 0;
}
# Local Variables:
# mode:perl
# End:
......@@ -374,6 +374,26 @@ window.ShowExtendModal = (function()
var hours = parseInt((diff / 1000) / 3600);
return (hours < 1 ? 1 : hours);
}
function HoursToEnglish(hours)
{
var days = parseInt(hours / 24);
var hours = hours % 24;
var str;
if (days) {
str = days + " days";
if (hours) {
str = str + " " + hours + " hours";
}
}
else if (hours) {
str = hours + " hours";
}
else {
str = "nothing";
}
}
//
// Request experiment extension.
......@@ -418,9 +438,10 @@ window.ShowExtendModal = (function()
"howlong": howlong,
"reason" : reason});
xmlthing.done(function(json) {
sup.HideModal("#waitwait-modal");
console.info(json.value);
callback(json);
sup.HideModal("#waitwait-modal", function () {
callback(json);
});
return;
});
}
......
......@@ -767,9 +767,25 @@ $(function ()
}
var expiration = json.value.expiration;
$("#quickvm_expires").html(moment(expiration).format('lll'));
// Reset the countdown clock.
StartCountdownClock.reset = expiration;
// Warn the user if we granted nothing.
if (json.value.granted == 0) {
if (json.value.message != "") {
$('#no-extension-granted-modal .reason')
.text(json.value.message);
$('#no-extension-granted-modal .reason-div')
.removeClass("hidden");
}
else {
$('#no-extension-granted-modal .reason')
.text("");
$('#no-extension-granted-modal .reason-div')
.addClass("hidden");
}
sup.ShowModal('#no-extension-granted-modal');
}
}
//
......
......@@ -381,7 +381,7 @@ function Do_RequestExtension()
$force = 1;
}
}
else if ($wanted > 24) {
else if ($wanted >= 24) {
if (!isset($ajax_args["reason"]) || $ajax_args["reason"] == "") {
SPITAJAX_ERROR(1, "Missing reason");
goto bad;
......@@ -433,7 +433,9 @@ function Do_RequestExtension()
}
# Refresh.
$slice = GeniSlice::Lookup("sa", $instance->slice_uuid());
$blob = array("expiration" => DateStringGMT($slice->expires()));
$blob = array("expiration" => DateStringGMT($slice->expires()),
"granted" => $webtask->TaskValue("granted"),
"message" => $webtask->TaskValue("message"));